rapns 2.0.5 → 3.0.0.beta.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|