rapns 2.0.5 → 3.0.0.beta.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.
- data/lib/generators/rapns_generator.rb +1 -0
- data/lib/generators/templates/add_gcm.rb +86 -0
- data/lib/generators/templates/create_rapns_notifications.rb +1 -1
- data/lib/rapns/apns/app.rb +8 -0
- data/lib/rapns/apns/binary_notification_validator.rb +12 -0
- data/lib/rapns/apns/device_token_format_validator.rb +12 -0
- data/lib/rapns/apns/feedback.rb +14 -0
- data/lib/rapns/apns/notification.rb +84 -0
- data/lib/rapns/app.rb +5 -6
- data/lib/rapns/{config.rb → configuration.rb} +5 -5
- data/lib/rapns/daemon/apns/app_runner.rb +36 -0
- data/lib/rapns/daemon/apns/connection.rb +113 -0
- data/lib/rapns/daemon/apns/delivery.rb +63 -0
- data/lib/rapns/daemon/apns/delivery_handler.rb +21 -0
- data/lib/rapns/daemon/apns/disconnection_error.rb +20 -0
- data/lib/rapns/daemon/apns/feedback_receiver.rb +74 -0
- data/lib/rapns/daemon/app_runner.rb +76 -77
- data/lib/rapns/daemon/database_reconnectable.rb +3 -3
- data/lib/rapns/daemon/delivery.rb +43 -0
- data/lib/rapns/daemon/delivery_error.rb +6 -2
- data/lib/rapns/daemon/delivery_handler.rb +13 -79
- data/lib/rapns/daemon/delivery_queue_18.rb +2 -2
- data/lib/rapns/daemon/delivery_queue_19.rb +3 -3
- data/lib/rapns/daemon/feeder.rb +5 -5
- data/lib/rapns/daemon/gcm/app_runner.rb +13 -0
- data/lib/rapns/daemon/gcm/delivery.rb +206 -0
- data/lib/rapns/daemon/gcm/delivery_handler.rb +20 -0
- data/lib/rapns/daemon.rb +31 -20
- data/lib/rapns/gcm/app.rb +7 -0
- data/lib/rapns/gcm/expiry_collapse_key_mutual_inclusion_validator.rb +11 -0
- data/lib/rapns/gcm/notification.rb +31 -0
- data/lib/rapns/gcm/payload_size_validator.rb +13 -0
- data/lib/rapns/multi_json_helper.rb +16 -0
- data/lib/rapns/notification.rb +28 -95
- data/lib/rapns/version.rb +1 -1
- data/lib/rapns.rb +14 -4
- data/lib/tasks/cane.rake +19 -0
- data/lib/tasks/test.rake +34 -0
- data/spec/acceptance/gcm_upgrade_spec.rb +34 -0
- data/spec/acceptance_spec_helper.rb +85 -0
- data/spec/support/simplecov_helper.rb +13 -0
- data/spec/support/simplecov_quality_formatter.rb +8 -0
- data/spec/unit/apns/app_spec.rb +15 -0
- data/spec/unit/apns/feedback_spec.rb +12 -0
- data/spec/{rapns → unit/apns}/notification_spec.rb +44 -72
- data/spec/unit/app_spec.rb +18 -0
- data/spec/unit/daemon/apns/app_runner_spec.rb +37 -0
- data/spec/{rapns/daemon → unit/daemon/apns}/connection_spec.rb +9 -9
- data/spec/unit/daemon/apns/delivery_handler_spec.rb +48 -0
- data/spec/unit/daemon/apns/delivery_spec.rb +154 -0
- data/spec/{rapns/daemon → unit/daemon/apns}/feedback_receiver_spec.rb +14 -14
- data/spec/unit/daemon/app_runner_shared.rb +66 -0
- data/spec/unit/daemon/app_runner_spec.rb +78 -0
- data/spec/{rapns → unit}/daemon/database_reconnectable_spec.rb +4 -5
- data/spec/{rapns → unit}/daemon/delivery_error_spec.rb +2 -2
- data/spec/unit/daemon/delivery_handler_shared.rb +19 -0
- data/spec/{rapns → unit}/daemon/delivery_queue_spec.rb +1 -1
- data/spec/{rapns → unit}/daemon/feeder_spec.rb +33 -33
- data/spec/unit/daemon/gcm/app_runner_spec.rb +15 -0
- data/spec/unit/daemon/gcm/delivery_handler_spec.rb +36 -0
- data/spec/unit/daemon/gcm/delivery_spec.rb +236 -0
- data/spec/{rapns → unit}/daemon/interruptible_sleep_spec.rb +1 -1
- data/spec/{rapns → unit}/daemon/logger_spec.rb +1 -1
- data/spec/{rapns → unit}/daemon_spec.rb +1 -1
- data/spec/unit/gcm/app_spec.rb +5 -0
- data/spec/unit/gcm/notification_spec.rb +55 -0
- data/spec/unit/notification_shared.rb +38 -0
- data/spec/unit/notification_spec.rb +6 -0
- data/spec/{rapns/app_spec.rb → unit_spec_helper.rb} +76 -16
- metadata +107 -45
- data/lib/rapns/binary_notification_validator.rb +0 -10
- data/lib/rapns/daemon/connection.rb +0 -114
- data/lib/rapns/daemon/delivery_handler_pool.rb +0 -18
- data/lib/rapns/daemon/disconnection_error.rb +0 -14
- data/lib/rapns/daemon/feedback_receiver.rb +0 -82
- data/lib/rapns/device_token_format_validator.rb +0 -10
- data/lib/rapns/feedback.rb +0 -12
- data/spec/rapns/daemon/app_runner_spec.rb +0 -193
- data/spec/rapns/daemon/delivery_handler_pool_spec.rb +0 -17
- data/spec/rapns/daemon/delivery_handler_spec.rb +0 -206
- data/spec/rapns/feedback_spec.rb +0 -12
- data/spec/spec_helper.rb +0 -78
@@ -0,0 +1,206 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Daemon
|
3
|
+
module Gcm
|
4
|
+
# http://developer.android.com/guide/google/gcm/gcm.html#response
|
5
|
+
class Delivery < Rapns::Daemon::Delivery
|
6
|
+
include Rapns::MultiJsonHelper
|
7
|
+
|
8
|
+
GCM_URI = URI.parse('https://android.googleapis.com/gcm/send')
|
9
|
+
UNAVAILABLE_STATES = ['Unavailable', 'InternalServerError']
|
10
|
+
|
11
|
+
def initialize(app, http, notification)
|
12
|
+
@app = app
|
13
|
+
@http = http
|
14
|
+
@notification = notification
|
15
|
+
end
|
16
|
+
|
17
|
+
def perform
|
18
|
+
begin
|
19
|
+
handle_response(do_post)
|
20
|
+
rescue Rapns::DeliveryError => error
|
21
|
+
mark_failed(error.code, error.description)
|
22
|
+
raise
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
def handle_response(response)
|
29
|
+
case response.code.to_i
|
30
|
+
when 200
|
31
|
+
ok(response)
|
32
|
+
when 400
|
33
|
+
bad_request(response)
|
34
|
+
when 401
|
35
|
+
unauthorized(response)
|
36
|
+
when 500
|
37
|
+
internal_server_error(response)
|
38
|
+
when 503
|
39
|
+
service_unavailable(response)
|
40
|
+
else
|
41
|
+
raise Rapns::DeliveryError.new(response.code, @notification.id, HTTP_STATUS_CODES[response.code.to_i])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def ok(response)
|
46
|
+
body = multi_json_load(response.body)
|
47
|
+
|
48
|
+
if body['failure'].to_i == 0
|
49
|
+
mark_delivered
|
50
|
+
Rapns::Daemon.logger.info("[#{@app.name}] #{@notification.id} sent to #{@notification.registration_ids.join(', ')}")
|
51
|
+
else
|
52
|
+
handle_errors(response, body)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def handle_errors(response, body)
|
57
|
+
errors = {}
|
58
|
+
|
59
|
+
body['results'].each_with_index do |result, i|
|
60
|
+
errors[i] = result['error'] if result['error']
|
61
|
+
end
|
62
|
+
|
63
|
+
if body['success'].to_i == 0 && errors.values.all? { |error| error.in?(UNAVAILABLE_STATES) }
|
64
|
+
all_devices_unavailable(response)
|
65
|
+
elsif errors.values.any? { |error| error.in?(UNAVAILABLE_STATES) }
|
66
|
+
some_devices_unavailable(response, errors)
|
67
|
+
else
|
68
|
+
raise Rapns::DeliveryError.new(nil, @notification.id, describe_errors(errors))
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def bad_request(response)
|
73
|
+
raise Rapns::DeliveryError.new(400, @notification.id, 'GCM failed to parse the JSON request. Possibly an rapns bug, please open an issue.')
|
74
|
+
end
|
75
|
+
|
76
|
+
def unauthorized(response)
|
77
|
+
raise Rapns::DeliveryError.new(401, @notification.id, 'Unauthorized, check your App auth_key.')
|
78
|
+
end
|
79
|
+
|
80
|
+
def internal_server_error(response)
|
81
|
+
retry_delivery(@notification, response)
|
82
|
+
Rapns::Daemon.logger.warn("GCM responded with an Internal Error. " + retry_message)
|
83
|
+
end
|
84
|
+
|
85
|
+
def service_unavailable(response)
|
86
|
+
retry_delivery(@notification, response)
|
87
|
+
Rapns::Daemon.logger.warn("GCM responded with an Service Unavailable Error. " + retry_message)
|
88
|
+
end
|
89
|
+
|
90
|
+
def all_devices_unavailable(response)
|
91
|
+
retry_delivery(@notification, response)
|
92
|
+
Rapns::Daemon.logger.warn("All recipients unavailable. " + retry_message)
|
93
|
+
end
|
94
|
+
|
95
|
+
def some_devices_unavailable(response, errors)
|
96
|
+
unavailable_idxs = errors.find_all { |i, error| error.in?(UNAVAILABLE_STATES) }.map(&:first)
|
97
|
+
new_notification = build_new_notification(response, unavailable_idxs)
|
98
|
+
with_database_reconnect_and_retry { new_notification.save! }
|
99
|
+
raise Rapns::DeliveryError.new(nil, @notification.id,
|
100
|
+
describe_errors(errors) + " #{unavailable_idxs.join(', ')} will be retried as notification #{new_notification.id}.")
|
101
|
+
end
|
102
|
+
|
103
|
+
def build_new_notification(response, idxs)
|
104
|
+
notification = Rapns::Gcm::Notification.new
|
105
|
+
notification.assign_attributes(@notification.attributes.slice('app_id', 'collapse_key', 'delay_while_idle'))
|
106
|
+
notification.data = @notification.data
|
107
|
+
notification.registration_ids = idxs.map { |i| @notification.registration_ids[i] }
|
108
|
+
notification.deliver_after = deliver_after_header(response)
|
109
|
+
notification
|
110
|
+
end
|
111
|
+
|
112
|
+
def deliver_after_header(response)
|
113
|
+
if response.header['retry-after']
|
114
|
+
retry_after = if response.header['retry-after'].to_s =~ /^[0-9]+$/
|
115
|
+
Time.now + response.header['retry-after'].to_i
|
116
|
+
else
|
117
|
+
Time.httpdate(response.header['retry-after'])
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def retry_delivery(notification, response)
|
123
|
+
if time = deliver_after_header(response)
|
124
|
+
retry_after(notification, time)
|
125
|
+
else
|
126
|
+
retry_exponentially(notification)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def describe_errors(errors)
|
131
|
+
description = if errors.size == @notification.registration_ids.size
|
132
|
+
"Failed to deliver to all recipients. Errors: #{errors.values.join(', ')}."
|
133
|
+
else
|
134
|
+
"Failed to deliver to recipients #{errors.keys.join(', ')}. Errors: #{errors.values.join(', ')}."
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def retry_message
|
139
|
+
"Notification #{@notification.id} will be retired after #{@notification.deliver_after.strftime("%Y-%m-%d %H:%M:%S")} (retry #{@notification.retries})."
|
140
|
+
end
|
141
|
+
|
142
|
+
def do_post
|
143
|
+
post = Net::HTTP::Post.new(GCM_URI.path, initheader = {'Content-Type' => 'application/json',
|
144
|
+
'Authorization' => "key=#{@notification.app.auth_key}"})
|
145
|
+
post.body = @notification.as_json.to_json
|
146
|
+
@http.request(GCM_URI, post)
|
147
|
+
end
|
148
|
+
|
149
|
+
HTTP_STATUS_CODES = {
|
150
|
+
100 => 'Continue',
|
151
|
+
101 => 'Switching Protocols',
|
152
|
+
102 => 'Processing',
|
153
|
+
200 => 'OK',
|
154
|
+
201 => 'Created',
|
155
|
+
202 => 'Accepted',
|
156
|
+
203 => 'Non-Authoritative Information',
|
157
|
+
204 => 'No Content',
|
158
|
+
205 => 'Reset Content',
|
159
|
+
206 => 'Partial Content',
|
160
|
+
207 => 'Multi-Status',
|
161
|
+
226 => 'IM Used',
|
162
|
+
300 => 'Multiple Choices',
|
163
|
+
301 => 'Moved Permanently',
|
164
|
+
302 => 'Found',
|
165
|
+
303 => 'See Other',
|
166
|
+
304 => 'Not Modified',
|
167
|
+
305 => 'Use Proxy',
|
168
|
+
306 => 'Reserved',
|
169
|
+
307 => 'Temporary Redirect',
|
170
|
+
400 => 'Bad Request',
|
171
|
+
401 => 'Unauthorized',
|
172
|
+
402 => 'Payment Required',
|
173
|
+
403 => 'Forbidden',
|
174
|
+
404 => 'Not Found',
|
175
|
+
405 => 'Method Not Allowed',
|
176
|
+
406 => 'Not Acceptable',
|
177
|
+
407 => 'Proxy Authentication Required',
|
178
|
+
408 => 'Request Timeout',
|
179
|
+
409 => 'Conflict',
|
180
|
+
410 => 'Gone',
|
181
|
+
411 => 'Length Required',
|
182
|
+
412 => 'Precondition Failed',
|
183
|
+
413 => 'Request Entity Too Large',
|
184
|
+
414 => 'Request-URI Too Long',
|
185
|
+
415 => 'Unsupported Media Type',
|
186
|
+
416 => 'Requested Range Not Satisfiable',
|
187
|
+
417 => 'Expectation Failed',
|
188
|
+
418 => "I'm a Teapot",
|
189
|
+
422 => 'Unprocessable Entity',
|
190
|
+
423 => 'Locked',
|
191
|
+
424 => 'Failed Dependency',
|
192
|
+
426 => 'Upgrade Required',
|
193
|
+
500 => 'Internal Server Error',
|
194
|
+
501 => 'Not Implemented',
|
195
|
+
502 => 'Bad Gateway',
|
196
|
+
503 => 'Service Unavailable',
|
197
|
+
504 => 'Gateway Timeout',
|
198
|
+
505 => 'HTTP Version Not Supported',
|
199
|
+
506 => 'Variant Also Negotiates',
|
200
|
+
507 => 'Insufficient Storage',
|
201
|
+
510 => 'Not Extended',
|
202
|
+
}
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Daemon
|
3
|
+
module Gcm
|
4
|
+
class DeliveryHandler < Rapns::Daemon::DeliveryHandler
|
5
|
+
def initialize(app)
|
6
|
+
@app = app
|
7
|
+
@http = Net::HTTP::Persistent.new('rapns')
|
8
|
+
end
|
9
|
+
|
10
|
+
def deliver(notification)
|
11
|
+
Rapns::Daemon::Gcm::Delivery.perform(@app, @http, notification)
|
12
|
+
end
|
13
|
+
|
14
|
+
def stopped
|
15
|
+
@http.shutdown
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/rapns/daemon.rb
CHANGED
@@ -3,18 +3,28 @@ require 'socket'
|
|
3
3
|
require 'pathname'
|
4
4
|
require 'openssl'
|
5
5
|
|
6
|
+
require 'net/http/persistent'
|
7
|
+
|
6
8
|
require 'rapns/daemon/interruptible_sleep'
|
7
9
|
require 'rapns/daemon/delivery_error'
|
8
|
-
require 'rapns/daemon/disconnection_error'
|
9
|
-
require 'rapns/daemon/connection'
|
10
10
|
require 'rapns/daemon/database_reconnectable'
|
11
|
+
require 'rapns/daemon/delivery'
|
11
12
|
require 'rapns/daemon/delivery_queue'
|
12
|
-
require 'rapns/daemon/delivery_handler'
|
13
|
-
require 'rapns/daemon/delivery_handler_pool'
|
14
|
-
require 'rapns/daemon/feedback_receiver'
|
15
|
-
require 'rapns/daemon/app_runner'
|
16
13
|
require 'rapns/daemon/feeder'
|
17
14
|
require 'rapns/daemon/logger'
|
15
|
+
require 'rapns/daemon/app_runner'
|
16
|
+
require 'rapns/daemon/delivery_handler'
|
17
|
+
|
18
|
+
require 'rapns/daemon/apns/delivery'
|
19
|
+
require 'rapns/daemon/apns/disconnection_error'
|
20
|
+
require 'rapns/daemon/apns/connection'
|
21
|
+
require 'rapns/daemon/apns/app_runner'
|
22
|
+
require 'rapns/daemon/apns/delivery_handler'
|
23
|
+
require 'rapns/daemon/apns/feedback_receiver'
|
24
|
+
|
25
|
+
require 'rapns/daemon/gcm/delivery'
|
26
|
+
require 'rapns/daemon/gcm/app_runner'
|
27
|
+
require 'rapns/daemon/gcm/delivery_handler'
|
18
28
|
|
19
29
|
module Rapns
|
20
30
|
module Daemon
|
@@ -90,19 +100,6 @@ module Rapns
|
|
90
100
|
delete_pid_file
|
91
101
|
end
|
92
102
|
|
93
|
-
def self.daemonize
|
94
|
-
exit if pid = fork
|
95
|
-
Process.setsid
|
96
|
-
exit if pid = fork
|
97
|
-
|
98
|
-
Dir.chdir '/'
|
99
|
-
File.umask 0000
|
100
|
-
|
101
|
-
STDIN.reopen '/dev/null'
|
102
|
-
STDOUT.reopen '/dev/null', 'a'
|
103
|
-
STDERR.reopen STDOUT
|
104
|
-
end
|
105
|
-
|
106
103
|
def self.write_pid_file
|
107
104
|
if !config.pid_file.blank?
|
108
105
|
begin
|
@@ -117,5 +114,19 @@ module Rapns
|
|
117
114
|
pid_file = config.pid_file
|
118
115
|
File.delete(pid_file) if !pid_file.blank? && File.exists?(pid_file)
|
119
116
|
end
|
117
|
+
|
118
|
+
# :nocov:
|
119
|
+
def self.daemonize
|
120
|
+
exit if pid = fork
|
121
|
+
Process.setsid
|
122
|
+
exit if pid = fork
|
123
|
+
|
124
|
+
Dir.chdir '/'
|
125
|
+
File.umask 0000
|
126
|
+
|
127
|
+
STDIN.reopen '/dev/null'
|
128
|
+
STDOUT.reopen '/dev/null', 'a'
|
129
|
+
STDERR.reopen STDOUT
|
130
|
+
end
|
120
131
|
end
|
121
|
-
end
|
132
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Gcm
|
3
|
+
class ExpiryCollapseKeyMutualInclusionValidator < ActiveModel::Validator
|
4
|
+
def validate(record)
|
5
|
+
if record.collapse_key && !record.expiry
|
6
|
+
record.errors[:expiry] << "must be set when using a collapse_key"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Gcm
|
3
|
+
class Notification < Rapns::Notification
|
4
|
+
validates :registration_ids, :presence => true
|
5
|
+
validates_with Rapns::Gcm::ExpiryCollapseKeyMutualInclusionValidator
|
6
|
+
validates_with Rapns::Gcm::PayloadSizeValidator
|
7
|
+
|
8
|
+
def registration_ids=(ids)
|
9
|
+
ids = [ids] if ids && !ids.is_a?(Array)
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def as_json
|
14
|
+
json = {
|
15
|
+
'registration_ids' => registration_ids,
|
16
|
+
'delay_while_idle' => delay_while_idle,
|
17
|
+
'data' => data
|
18
|
+
}
|
19
|
+
|
20
|
+
if collapse_key
|
21
|
+
json.merge!({
|
22
|
+
'collapse_key' => collapse_key,
|
23
|
+
'time_to_live' => expiry
|
24
|
+
})
|
25
|
+
end
|
26
|
+
|
27
|
+
json
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Gcm
|
3
|
+
class PayloadSizeValidator < ActiveModel::Validator
|
4
|
+
LIMIT = 4096
|
5
|
+
|
6
|
+
def validate(record)
|
7
|
+
if record.payload_size > LIMIT
|
8
|
+
record.errors[:base] << "GCM notification payload cannot be larger than #{LIMIT} bytes."
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Rapns
|
2
|
+
module MultiJsonHelper
|
3
|
+
def multi_json_load(string, options = {})
|
4
|
+
# Calling load on multi_json less than v1.3.0 attempts to load a file from disk.
|
5
|
+
if Gem.loaded_specs['multi_json'].version >= Gem::Version.create('1.3.0')
|
6
|
+
MultiJson.load(string, options)
|
7
|
+
else
|
8
|
+
MultiJson.decode(string, options)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def multi_json_dump(string, options = {})
|
13
|
+
MultiJson.respond_to?(:dump) ? MultiJson.dump(string, options) : MultiJson.encode(string, options)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/rapns/notification.rb
CHANGED
@@ -1,90 +1,46 @@
|
|
1
1
|
module Rapns
|
2
2
|
class Notification < ActiveRecord::Base
|
3
|
-
|
3
|
+
include Rapns::MultiJsonHelper
|
4
4
|
|
5
|
-
|
6
|
-
:delivered_at, :failed, :failed_at, :error_code, :error_description, :deliver_after, :alert_is_json, :app
|
5
|
+
self.table_name = 'rapns_notifications'
|
7
6
|
|
8
|
-
|
9
|
-
|
10
|
-
validates :badge, :numericality => true, :allow_nil => true
|
11
|
-
validates :expiry, :numericality => true, :presence => true
|
7
|
+
# TODO: Dump using multi json.
|
8
|
+
serialize :registration_ids
|
12
9
|
|
13
|
-
|
14
|
-
validates_with Rapns::BinaryNotificationValidator
|
10
|
+
belongs_to :app, :class_name => 'Rapns::App'
|
15
11
|
|
16
|
-
|
12
|
+
attr_accessible :badge, :device_token, :sound, :alert, :data, :expiry,:delivered,
|
13
|
+
:delivered_at, :failed, :failed_at, :error_code, :error_description, :deliver_after,
|
14
|
+
:alert_is_json, :app, :app_id, :collapse_key, :delay_while_idle, :registration_ids
|
17
15
|
|
18
|
-
|
19
|
-
|
20
|
-
end
|
16
|
+
validates :expiry, :numericality => true, :allow_nil => true
|
17
|
+
validates :app, :presence => true
|
21
18
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
else
|
27
|
-
write_attribute(:alert, alert)
|
28
|
-
self.alert_is_json = false if has_attribute?(:alert_is_json)
|
29
|
-
end
|
30
|
-
end
|
19
|
+
scope :ready_for_delivery, lambda {
|
20
|
+
where('delivered = ? AND failed = ? AND (deliver_after IS NULL OR deliver_after < ?)',
|
21
|
+
false, false, Time.now)
|
22
|
+
}
|
31
23
|
|
32
|
-
|
33
|
-
|
24
|
+
scope :for_apps, lambda { |apps|
|
25
|
+
where(:app_id => apps.map(&:id))
|
26
|
+
}
|
34
27
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
string_or_json
|
40
|
-
end
|
41
|
-
else
|
42
|
-
multi_json_load(string_or_json) rescue string_or_json
|
28
|
+
def initialize(attributes = nil, options = {})
|
29
|
+
if attributes.is_a?(Hash) && attributes.keys.include?(:attributes_for_device)
|
30
|
+
msg = ":attributes_for_device via mass-assignment is deprecated. Use :data or the attributes_for_device= instance method."
|
31
|
+
ActiveSupport::Deprecation.warn(msg, caller(1))
|
43
32
|
end
|
33
|
+
super
|
44
34
|
end
|
45
35
|
|
46
|
-
def
|
47
|
-
|
48
|
-
|
36
|
+
def data=(attrs)
|
37
|
+
return unless attrs
|
38
|
+
raise ArgumentError, "must be a Hash" if !attrs.is_a?(Hash)
|
39
|
+
write_attribute(:data, multi_json_dump(attrs))
|
49
40
|
end
|
50
41
|
|
51
|
-
def
|
52
|
-
multi_json_load(read_attribute(:
|
53
|
-
end
|
54
|
-
|
55
|
-
MDM_KEY = '__rapns_mdm__'
|
56
|
-
def mdm=(magic)
|
57
|
-
self.attributes_for_device = { MDM_KEY => magic }
|
58
|
-
end
|
59
|
-
|
60
|
-
CONTENT_AVAILABLE_KEY = '__rapns_content_available__'
|
61
|
-
def content_available=(bool)
|
62
|
-
return unless bool
|
63
|
-
self.attributes_for_device = { CONTENT_AVAILABLE_KEY => true }
|
64
|
-
end
|
65
|
-
|
66
|
-
def as_json
|
67
|
-
json = ActiveSupport::OrderedHash.new
|
68
|
-
|
69
|
-
if attributes_for_device && attributes_for_device.key?(MDM_KEY)
|
70
|
-
json['mdm'] = attributes_for_device[MDM_KEY]
|
71
|
-
else
|
72
|
-
json['aps'] = ActiveSupport::OrderedHash.new
|
73
|
-
json['aps']['alert'] = alert if alert
|
74
|
-
json['aps']['badge'] = badge if badge
|
75
|
-
json['aps']['sound'] = sound if sound
|
76
|
-
|
77
|
-
if attributes_for_device && attributes_for_device[CONTENT_AVAILABLE_KEY]
|
78
|
-
json['aps']['content-available'] = 1
|
79
|
-
end
|
80
|
-
|
81
|
-
if attributes_for_device
|
82
|
-
non_aps_attributes = attributes_for_device.reject { |k, v| k == CONTENT_AVAILABLE_KEY }
|
83
|
-
non_aps_attributes.each { |k, v| json[k.to_s] = v.to_s }
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
json
|
42
|
+
def data
|
43
|
+
multi_json_load(read_attribute(:data)) if read_attribute(:data)
|
88
44
|
end
|
89
45
|
|
90
46
|
def payload
|
@@ -94,28 +50,5 @@ module Rapns
|
|
94
50
|
def payload_size
|
95
51
|
payload.bytesize
|
96
52
|
end
|
97
|
-
|
98
|
-
# This method conforms to the enhanced binary format.
|
99
|
-
# http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW4
|
100
|
-
def to_binary(options = {})
|
101
|
-
id_for_pack = options[:for_validation] ? 0 : id
|
102
|
-
[1, id_for_pack, expiry, 0, 32, device_token, payload_size, payload].pack("cNNccH*na*")
|
103
|
-
end
|
104
|
-
|
105
|
-
|
106
|
-
private
|
107
|
-
|
108
|
-
def multi_json_load(string, options = {})
|
109
|
-
# Calling load on multi_json less than v1.3.0 attempts to load a file from disk. Check the version explicitly.
|
110
|
-
if Gem.loaded_specs['multi_json'].version >= Gem::Version.create('1.3.0')
|
111
|
-
MultiJson.load(string, options)
|
112
|
-
else
|
113
|
-
MultiJson.decode(string, options)
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
def multi_json_dump(string, options = {})
|
118
|
-
MultiJson.respond_to?(:dump) ? MultiJson.dump(string, options) : MultiJson.encode(string, options)
|
119
|
-
end
|
120
53
|
end
|
121
54
|
end
|
data/lib/rapns/version.rb
CHANGED
data/lib/rapns.rb
CHANGED
@@ -2,9 +2,19 @@ require 'active_record'
|
|
2
2
|
require 'multi_json'
|
3
3
|
|
4
4
|
require 'rapns/version'
|
5
|
-
require 'rapns/
|
6
|
-
require 'rapns/device_token_format_validator'
|
5
|
+
require 'rapns/multi_json_helper'
|
7
6
|
require 'rapns/notification'
|
8
|
-
require 'rapns/feedback'
|
9
7
|
require 'rapns/app'
|
10
|
-
require 'rapns/
|
8
|
+
require 'rapns/configuration'
|
9
|
+
|
10
|
+
require 'rapns/apns/binary_notification_validator'
|
11
|
+
require 'rapns/apns/device_token_format_validator'
|
12
|
+
require 'rapns/apns/notification'
|
13
|
+
require 'rapns/apns/feedback'
|
14
|
+
require 'rapns/apns/app'
|
15
|
+
|
16
|
+
require 'rapns/gcm/expiry_collapse_key_mutual_inclusion_validator'
|
17
|
+
require 'rapns/gcm/payload_size_validator'
|
18
|
+
require 'rapns/gcm/notification'
|
19
|
+
require 'rapns/gcm/app'
|
20
|
+
|
data/lib/tasks/cane.rake
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
begin
|
2
|
+
require 'cane/rake_task'
|
3
|
+
|
4
|
+
desc "Run cane to check quality metrics"
|
5
|
+
Cane::RakeTask.new(:quality) do |cane|
|
6
|
+
cane.add_threshold 'coverage/covered_percent', :>=, 97
|
7
|
+
cane.no_style = false
|
8
|
+
cane.style_measure = 1000
|
9
|
+
cane.no_doc = true
|
10
|
+
cane.abc_max = 15
|
11
|
+
cane.abc_exclude = %w(Rapns::Daemon::Gcm::Delivery#handle_errors)
|
12
|
+
end
|
13
|
+
|
14
|
+
namespace :spec do
|
15
|
+
task :cane => ['spec', 'quality']
|
16
|
+
end
|
17
|
+
rescue LoadError
|
18
|
+
warn "cane not available, quality task not provided."
|
19
|
+
end
|
data/lib/tasks/test.rake
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
namespace :test do
|
2
|
+
task :build_rails do
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
def cmd(str)
|
6
|
+
puts "* #{str}"
|
7
|
+
retval = Bundler.with_clean_env { `#{str}` }
|
8
|
+
puts retval.strip
|
9
|
+
retval
|
10
|
+
end
|
11
|
+
|
12
|
+
rapns_root = Dir.pwd
|
13
|
+
path = '/tmp/rails_test'
|
14
|
+
cmd("rm -rf #{path}")
|
15
|
+
FileUtils.mkdir_p(path)
|
16
|
+
pwd = Dir.pwd
|
17
|
+
|
18
|
+
cmd("bundle exec rails new #{path} --skip-bundle")
|
19
|
+
branch = cmd("git branch | grep '\*'").split(' ').last
|
20
|
+
|
21
|
+
begin
|
22
|
+
Dir.chdir(path)
|
23
|
+
cmd('echo "gem \'rake\'" >> Gemfile')
|
24
|
+
cmd("echo \"gem 'rapns', :git => '#{rapns_root}', :branch => '#{branch}'\" >> Gemfile")
|
25
|
+
cmd('bundle install')
|
26
|
+
cmd('bundle exec rails g rapns')
|
27
|
+
cmd('bundle exec rake db:migrate')
|
28
|
+
ensure
|
29
|
+
Dir.chdir(pwd)
|
30
|
+
end
|
31
|
+
|
32
|
+
puts "Built into #{path}"
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'acceptance_spec_helper'
|
2
|
+
|
3
|
+
describe 'GCM upgrade' do
|
4
|
+
before do
|
5
|
+
setup_rails
|
6
|
+
generate
|
7
|
+
migrate('create_rapns_notifications', 'create_rapns_feedback',
|
8
|
+
'add_alert_is_json_to_rapns_notifications', 'add_app_to_rapns',
|
9
|
+
'create_rapns_apps')
|
10
|
+
|
11
|
+
as_test_rails_db do
|
12
|
+
now = Time.now.to_s(:db)
|
13
|
+
ActiveRecord::Base.connection.execute <<-SQL
|
14
|
+
INSERT INTO rapns_apps (key, environment, certificate, created_at, updated_at)
|
15
|
+
VALUES ('test', 'development', 'c3rt', '#{now}', '#{now}')
|
16
|
+
SQL
|
17
|
+
|
18
|
+
ActiveRecord::Base.connection.execute <<-SQL
|
19
|
+
INSERT INTO rapns_notifications (app, device_token, created_at, updated_at)
|
20
|
+
VALUES ('test', 't0k3n', '#{now}', '#{now}')
|
21
|
+
SQL
|
22
|
+
end
|
23
|
+
|
24
|
+
migrate('add_gcm')
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'associates apps and notifications' do
|
28
|
+
as_test_rails_db do
|
29
|
+
app = Rapns::Apns::App.first
|
30
|
+
app.name.should == 'test'
|
31
|
+
app.notifications.count.should == 1
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|