resque-aps 0.9.9 → 0.9.10
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/resque_aps/application.rb +142 -131
- data/lib/resque_aps/feedback.rb +66 -66
- data/lib/resque_aps/notification.rb +178 -172
- data/lib/resque_aps/version.rb +1 -1
- data/lib/resque_aps.rb +135 -115
- metadata +25 -46
@@ -3,166 +3,177 @@ require 'openssl'
|
|
3
3
|
module Resque
|
4
4
|
module Plugins
|
5
5
|
module Aps
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
class Application
|
7
|
+
include Resque::Plugins::Aps::Helper
|
8
|
+
extend Resque::Plugins::Aps::Helper
|
9
9
|
|
10
|
-
|
10
|
+
attr_accessor :name, :cert_file, :cert_passwd
|
11
11
|
|
12
|
-
|
12
|
+
@queue = "apple_push_service"
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
14
|
+
def inspect
|
15
|
+
"#<#{self.class.name} #{name.inspect}, #{cert_passwd.inspect}, #{cert_file.inspect}>"
|
16
|
+
end
|
17
17
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
18
|
+
def self.perform(*args)
|
19
|
+
count = 0
|
20
|
+
start = Time.now
|
21
|
+
app_name = args[0]
|
22
|
+
Resque.aps_application(app_name).socket do |socket, app|
|
23
|
+
while true
|
24
|
+
n = Resque.dequeue_aps(app_name)
|
25
|
+
if n.nil?
|
26
|
+
if app.aps_nil_notification_retry? count, start
|
27
|
+
next
|
28
|
+
else
|
29
|
+
break
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
app.before_aps_write n
|
34
|
+
begin
|
35
|
+
n.batch_id = count + 1
|
36
|
+
n.expiry = (Time.now.utc + 1.hour).to_i
|
37
|
+
socket.write(n.formatted)
|
38
|
+
app.after_aps_write n
|
39
|
+
count += 1
|
40
|
+
unless (resp = socket.read).blank?
|
41
|
+
logger.error "Failure response: #{resp.inspect}" if logger
|
42
|
+
break
|
43
|
+
end
|
44
|
+
rescue
|
45
|
+
logger.error Application.application_exception($!, app_name) if logger
|
46
|
+
app.failed_aps_write n, $!
|
47
|
+
logger.error "Sent #{count} notifications before failure." if logger
|
48
|
+
redis.rpush(aps_application_queue_key(app_name), encode(n.to_hash))
|
49
|
+
break
|
50
|
+
end
|
30
51
|
end
|
31
52
|
end
|
53
|
+
logger.info("Sent #{count} #{app_name} notifications in batch over #{Time.now - start} sec.") if logger
|
54
|
+
ensure
|
55
|
+
Resque.dequeue_aps_application(app_name)
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# Create the TCP and SSL sockets for sending the notification
|
60
|
+
#
|
61
|
+
def self.create_sockets(cert, passphrase, host, port)
|
62
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
63
|
+
ctx.key = OpenSSL::PKey::RSA.new(cert, passphrase)
|
64
|
+
ctx.cert = OpenSSL::X509::Certificate.new(cert)
|
65
|
+
|
66
|
+
s = TCPSocket.new(host, port)
|
67
|
+
ssl = OpenSSL::SSL::SSLSocket.new(s, ctx)
|
68
|
+
ssl.sync = true
|
69
|
+
|
70
|
+
return s, ssl
|
71
|
+
end
|
32
72
|
|
33
|
-
|
73
|
+
#
|
74
|
+
# Close the sockets
|
75
|
+
#
|
76
|
+
def self.close_sockets(socket, ssl_socket)
|
34
77
|
begin
|
35
|
-
|
36
|
-
|
37
|
-
|
78
|
+
if ssl_socket
|
79
|
+
ssl_socket.close
|
80
|
+
end
|
38
81
|
rescue
|
39
|
-
logger.error
|
40
|
-
app.failed_aps_write n, $!
|
82
|
+
Resque.logger.error("#{$!}: #{$!.backtrace.join("\n")}") if Resque.logger
|
41
83
|
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
logger.info("Sent #{count} #{app_name} notifications in batch over #{Time.now - start} sec.") if logger
|
45
|
-
end
|
46
|
-
|
47
|
-
#
|
48
|
-
# Create the TCP and SSL sockets for sending the notification
|
49
|
-
#
|
50
|
-
def self.create_sockets(cert, passphrase, host, port)
|
51
|
-
ctx = OpenSSL::SSL::SSLContext.new
|
52
|
-
ctx.key = OpenSSL::PKey::RSA.new(cert, passphrase)
|
53
|
-
ctx.cert = OpenSSL::X509::Certificate.new(cert)
|
54
|
-
|
55
|
-
s = TCPSocket.new(host, port)
|
56
|
-
ssl = OpenSSL::SSL::SSLSocket.new(s, ctx)
|
57
|
-
ssl.sync = true
|
58
|
-
|
59
|
-
return s, ssl
|
60
|
-
end
|
61
84
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
85
|
+
begin
|
86
|
+
if socket
|
87
|
+
socket.close
|
88
|
+
end
|
89
|
+
rescue
|
90
|
+
Resque.logger.error("#{$!}: #{$!.backtrace.join("\n")}") if Resque.logger
|
91
|
+
end
|
69
92
|
end
|
70
|
-
rescue
|
71
|
-
Resque.logger.error("#{$!}: #{$!.backtrace.join("\n")}") if Resque.logger
|
72
|
-
end
|
73
93
|
|
74
|
-
|
75
|
-
|
76
|
-
|
94
|
+
def self.application_exception(exception, name)
|
95
|
+
exc = Exception.new("#{exception} (#{name})")
|
96
|
+
exc.set_backtrace(exception.backtrace)
|
97
|
+
return exc
|
77
98
|
end
|
78
|
-
rescue
|
79
|
-
Resque.logger.error("#{$!}: #{$!.backtrace.join("\n")}") if Resque.logger
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
def self.application_exception(exception, name)
|
84
|
-
exc = Exception.new("#{exception} (#{name})")
|
85
|
-
exc.set_backtrace(exception.backtrace)
|
86
|
-
return exc
|
87
|
-
end
|
88
99
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
100
|
+
def initialize(attributes)
|
101
|
+
attributes.each do |k, v|
|
102
|
+
respond_to?(:"#{k}=") ? send(:"#{k}=", v) : raise(Resque::Plugins::Aps::UnknownAttributeError, "unknown attribute: #{k}")
|
103
|
+
end
|
104
|
+
end
|
94
105
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
106
|
+
def socket(cert = nil, certp = nil, host = nil, port = nil, &block)
|
107
|
+
logger.debug("resque-aps: ssl_socket(#{name})") if logger
|
108
|
+
exc = nil
|
109
|
+
|
110
|
+
begin
|
111
|
+
socket, ssl_socket = Application.create_sockets(cert || File.read(cert_file),
|
112
|
+
certp || cert_passwd,
|
113
|
+
host || Resque.aps_gateway_host,
|
114
|
+
port || Resque.aps_gateway_port)
|
115
|
+
rescue
|
116
|
+
raise Application.application_exception($!, name)
|
117
|
+
end
|
107
118
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
119
|
+
begin
|
120
|
+
ssl_socket.connect
|
121
|
+
yield ssl_socket, self if block_given?
|
122
|
+
rescue
|
123
|
+
exc = Application.application_exception($!, name)
|
124
|
+
if $!.message =~ /^SSL_connect .* certificate (expired|revoked)/
|
125
|
+
notify_aps_admin exc
|
126
|
+
end
|
127
|
+
raise exc
|
128
|
+
ensure
|
129
|
+
Application.close_sockets(socket, ssl_socket)
|
130
|
+
end
|
120
131
|
|
121
|
-
|
122
|
-
|
132
|
+
exc
|
133
|
+
end
|
123
134
|
|
124
|
-
|
125
|
-
|
126
|
-
|
135
|
+
def to_hash
|
136
|
+
{'name' => name, 'cert_file' => cert_file, 'cert_passwd' => cert_passwd}
|
137
|
+
end
|
127
138
|
|
128
|
-
|
129
|
-
|
130
|
-
|
139
|
+
def to_json
|
140
|
+
to_hash.to_json
|
141
|
+
end
|
131
142
|
|
132
|
-
|
133
|
-
|
134
|
-
|
143
|
+
def before_aps_write(notification)
|
144
|
+
logger.debug("ResqueAps[before_write]: #{notification}") if logger
|
145
|
+
end
|
135
146
|
|
136
|
-
|
137
|
-
|
138
|
-
|
147
|
+
def after_aps_write(notification)
|
148
|
+
logger.debug("ResqueAps[after_write]: #{notification}") if logger
|
149
|
+
end
|
139
150
|
|
140
|
-
|
141
|
-
|
142
|
-
|
151
|
+
def failed_aps_write(notification, exception)
|
152
|
+
logger.error("ResqueAps[write_failed]: #{exception} (#{notification}): #{exception.backtrace.join("\n")}") if logger
|
153
|
+
end
|
143
154
|
|
144
|
-
|
145
|
-
|
155
|
+
def notify_aps_admin(exception)
|
156
|
+
end
|
146
157
|
|
147
|
-
|
148
|
-
|
149
|
-
|
158
|
+
def aps_nil_notification_retry?(sent_count, start_time)
|
159
|
+
false
|
160
|
+
end
|
150
161
|
|
151
|
-
|
152
|
-
|
153
|
-
|
162
|
+
def before_aps_read
|
163
|
+
logger.debug("ResqueAps[before_read]:") if logger
|
164
|
+
end
|
154
165
|
|
155
|
-
|
156
|
-
|
157
|
-
|
166
|
+
def after_aps_read(feedback)
|
167
|
+
logger.debug("ResqueAps[after_read]: #{feedback.to_s}") if logger
|
168
|
+
end
|
158
169
|
|
159
|
-
|
160
|
-
|
161
|
-
|
170
|
+
def aps_read_error(exception)
|
171
|
+
logger.error("ResqueAps[read_error]: #{exception} (#{name}): #{exception.backtrace.join("\n")}") if logger
|
172
|
+
end
|
162
173
|
|
163
|
-
|
164
|
-
|
165
|
-
|
174
|
+
def aps_read_failed
|
175
|
+
logger.error("ResqueAps[read_failed]: Bad data on the socket (#{name})") if logger
|
176
|
+
end
|
166
177
|
|
167
178
|
end
|
168
179
|
end
|
data/lib/resque_aps/feedback.rb
CHANGED
@@ -3,90 +3,90 @@ require 'timeout'
|
|
3
3
|
module Resque
|
4
4
|
module Plugins
|
5
5
|
module Aps
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
class Feedback
|
7
|
+
include Resque::Plugins::Aps::Helper
|
8
|
+
extend Resque::Plugins::Aps::Helper
|
9
9
|
|
10
|
-
|
10
|
+
@queue = "apple_push_service"
|
11
11
|
|
12
|
-
|
12
|
+
attr_accessor :application_name, :device_token, :received_at
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
14
|
+
def initialize(attributes)
|
15
|
+
attributes.each do |k, v|
|
16
|
+
respond_to?(:"#{k}=") ? send(:"#{k}=", v) : raise(Resque::Plugins::Aps::UnknownAttributeError, "unknown attribute: #{k}")
|
17
|
+
end
|
18
|
+
end
|
19
19
|
|
20
|
-
|
21
|
-
|
22
|
-
|
20
|
+
def inspect
|
21
|
+
"#<#{self.class.name} #{application_name.inspect}, #{device_token.inspect}, #{received_at.inspect}>"
|
22
|
+
end
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
24
|
+
def to_s
|
25
|
+
"#{application_name} #{received_at} #{device_token}"
|
26
|
+
end
|
27
27
|
|
28
|
-
|
29
|
-
|
30
|
-
|
28
|
+
def to_hash
|
29
|
+
{:application_name => application_name, :device_token => device_token, :received_at => received_at}
|
30
|
+
end
|
31
31
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
32
|
+
def self.read_feedback(ssl_socket, application_name, product_id)
|
33
|
+
data_str = ssl_socket.read(4)
|
34
|
+
return nil unless data_str
|
35
|
+
data_ary = data_str.unpack('N')
|
36
|
+
return nil unless data_ary && data_ary[0]
|
37
|
+
time = Time.at(data_ary[0])
|
38
38
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
39
|
+
data_str = ssl_socket.read(2)
|
40
|
+
return nil unless data_str
|
41
|
+
data_ary = data_str.unpack('n')
|
42
|
+
return nil unless data_ary && data_ary[0]
|
43
|
+
tl = data_ary[0]
|
44
44
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
45
|
+
data_str = ssl_socket.read(tl)
|
46
|
+
return nil unless data_str
|
47
|
+
data_ary = data_str.unpack('H*')
|
48
|
+
return nil unless data_ary && data_ary[0]
|
49
|
+
token = data_ary[0]
|
50
50
|
|
51
|
-
|
52
|
-
|
53
|
-
|
51
|
+
feedback = Feedback.new({:received_at => time, :device_token => token, :application_name => application_name})
|
52
|
+
return feedback
|
53
|
+
end
|
54
54
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
55
|
+
#
|
56
|
+
# Perform a Feedback check on the APN server, for the given app key (which must be the first argument)
|
57
|
+
#
|
58
|
+
def self.perform(*args)
|
59
|
+
app_name = args[0]
|
60
|
+
start = Time.now
|
61
|
+
count = 0
|
62
|
+
appl = Resque.aps_application(app_name)
|
63
63
|
|
64
|
-
|
64
|
+
return unless appl
|
65
65
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
66
|
+
appl.socket(nil, nil, Resque.aps_feedback_host, Resque.aps_feedback_port) do |socket, app|
|
67
|
+
begin
|
68
|
+
logger.debug("Feedback: Reading feedbacks for #{app_name}.") if logger
|
69
|
+
timeout(5) do
|
70
|
+
until socket.eof?
|
71
|
+
app.before_aps_read
|
72
|
+
feedback = read_feedback(socket, app_name, product_id)
|
73
|
+
if feedback
|
74
|
+
count += 1
|
75
|
+
app.after_aps_read(feedback)
|
76
|
+
else
|
77
|
+
app.aps_read_failed
|
78
|
+
end
|
79
|
+
end
|
78
80
|
end
|
81
|
+
rescue
|
82
|
+
logger.error Application.application_exception($!, app_name) if logger
|
83
|
+
app.aps_read_error(Application.application_exception($!, app_name))
|
79
84
|
end
|
80
85
|
end
|
81
|
-
|
82
|
-
logger.error Application.application_exception($!, app_name) if logger
|
83
|
-
app.aps_read_error(Application.application_exception($!, app_name))
|
86
|
+
logger.info("Read #{count} #{app_name} feedbacks over #{Time.now - start} sec.") if logger
|
84
87
|
end
|
88
|
+
|
85
89
|
end
|
86
|
-
logger.info("Read #{count} #{app_name} feedbacks over #{Time.now - start} sec.") if logger
|
87
90
|
end
|
88
|
-
|
89
|
-
end
|
90
91
|
end
|
91
|
-
end
|
92
92
|
end
|
@@ -1,197 +1,203 @@
|
|
1
1
|
module Resque
|
2
2
|
module Plugins
|
3
3
|
module Aps
|
4
|
-
|
5
|
-
|
6
|
-
|
4
|
+
class Notification
|
5
|
+
include Resque::Plugins::Aps::Helper
|
6
|
+
extend Resque::Plugins::Aps::Helper
|
7
7
|
|
8
|
-
|
8
|
+
attr_accessor :application_name, :device_token, :payload, :batch_id, :expiry
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
10
|
+
def initialize(attributes)
|
11
|
+
attributes.each do |k, v|
|
12
|
+
respond_to?(:"#{k}=") ? send(:"#{k}=", v) : raise(Resque::Plugins::Aps::UnknownAttributeError, "unknown attribute: #{k}")
|
13
|
+
end
|
14
|
+
end
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
|
16
|
+
def inspect
|
17
|
+
"#<#{self.class.name} #{application_name.inspect}, #{device_token.inspect}, #{payload.inspect}>"
|
18
|
+
end
|
19
19
|
|
20
|
-
|
21
|
-
|
22
|
-
|
20
|
+
def to_s
|
21
|
+
"#{device_token.inspect}, #{payload.inspect}"
|
22
|
+
end
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
24
|
+
def to_hash
|
25
|
+
{:application_name => application_name, :device_token => device_token, :payload => payload}
|
26
|
+
end
|
27
27
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
28
|
+
# SSL Configuration
|
29
|
+
# open Keychain Access, and export the "Apple Development Push" certificate associated with your app in p12 format
|
30
|
+
# Convert the certificate to PEM using openssl:
|
31
|
+
# openssl pkcs12 -in mycert.p12 -out client-cert.pem -nodes -clcerts
|
32
32
|
|
33
|
-
|
34
|
-
|
33
|
+
# To use with cross application push
|
34
|
+
# Notification.new(:user => user, :current_product => product, :cross_app => { :capabilities => 'cross_app', :payload => '{aps.....}' })
|
35
35
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
36
|
+
#
|
37
|
+
# https://developer.apple.com/iphone/prerelease/library/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW1
|
38
|
+
#
|
39
|
+
# Table 2-1 Keys and values of the aps dictionary
|
40
|
+
# alert | string or dictionary
|
41
|
+
# => If this property is included, iPhone OS displays a standard alert. You may specify a string as the value of alert or a dictionary as its value.
|
42
|
+
# => If you specify a string, it becomes the message text of an alert with two buttons: Close and View. If the user taps View, the application is launched.
|
43
|
+
# => Alternatively, you can specify a dictionary as the value of alert. See Table 2-2 for descriptions of the keys of this dictionary.
|
44
|
+
#
|
45
|
+
# badge | number
|
46
|
+
# => The number to display as the badge of the application icon. If this property is absent, any badge number currently shown is removed.
|
47
|
+
#
|
48
|
+
# sound | string
|
49
|
+
# => The name of a sound file in the application bundle. The sound in this file is played as an alert.
|
50
|
+
# => If the sound file doesn’t exist or default is specified as the value, the default alert sound is played.
|
51
|
+
# => The audio must be in one of the audio data formats that are compatible with system sounds; see “Preparing Custom Alert Sounds” for details.
|
52
|
+
#
|
53
|
+
# Table 2-2 Child properties of the alert property
|
54
|
+
# body | string
|
55
|
+
# => The text of the alert message.
|
56
|
+
#
|
57
|
+
# action-loc-key | string or null
|
58
|
+
# => If a string is specified, displays an alert with two buttons, whose behavior is described in Table 2-1.
|
59
|
+
# => However, iPhone OS uses the string as a key to get a localized string in the current localization to use for the right button’s title instead of “View”.
|
60
|
+
# => If the value is null, the system displays an alert with a single OK button that simply dismisses the alert when tapped.
|
61
|
+
#
|
62
|
+
# loc-key | string
|
63
|
+
# => A key to an alert-message string in a Localizable.strings file for the current localization (which is set by the user’s language preference).
|
64
|
+
# => The key string can be formatted with %@ and %n$@ specifiers to take the variables specified in loc-args. See “Localized Formatted Strings” for more information.
|
65
|
+
#
|
66
|
+
# loc-args | array of strings
|
67
|
+
# => Variable string values to appear in place of the format specifiers in loc-key. See “Localized Formatted Strings” for more information.
|
68
|
+
#
|
69
|
+
#
|
70
|
+
# Example Result:
|
71
|
+
# {
|
72
|
+
# "aps" : {
|
73
|
+
# "alert" : "You got your emails.",
|
74
|
+
# "badge" : 9,
|
75
|
+
# "sound" : "bingbong.aiff"
|
76
|
+
# },
|
77
|
+
# "acme1" : "bar",
|
78
|
+
# "acme2" : 42
|
79
|
+
#}
|
80
|
+
# Or
|
81
|
+
# {
|
82
|
+
# "aps" : {
|
83
|
+
# "alert" : {
|
84
|
+
# "action-loc-key" : "PLAY",
|
85
|
+
# "loc-key" : "SERVER.ERROR"
|
86
|
+
# "loc-args" : ["bob", "sierra"]
|
87
|
+
# },
|
88
|
+
# "badge" : 9,
|
89
|
+
# "sound" : "bingbong.aiff"
|
90
|
+
# },
|
91
|
+
# "acme1" : "bar",
|
92
|
+
# "acme2" : 42
|
93
|
+
#}
|
94
|
+
def self.to_payload(alert = nil, badge = nil, sound = nil, app_data = nil)
|
95
|
+
result = ActiveSupport::OrderedHash.new
|
96
|
+
result['aps'] = ActiveSupport::OrderedHash.new
|
97
|
+
result['aps']['alert'] = self.format_alert(alert) unless alert.blank?
|
98
|
+
result['aps']['badge'] = badge.to_i unless badge.blank?
|
99
|
+
result['aps']['sound'] = sound unless sound.blank?
|
100
|
+
result.merge!(app_data) unless app_data.blank?
|
101
|
+
self.to_json(result)
|
102
|
+
end
|
103
103
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
104
|
+
#
|
105
|
+
# Create an ordered hash of the data in the given alert hash
|
106
|
+
#
|
107
|
+
def self.format_alert(alert)
|
108
|
+
if alert.is_a? Hash
|
109
|
+
result = ActiveSupport::OrderedHash.new
|
110
|
+
result['action-loc-key'] = alert['action-loc-key'] unless alert['action-loc-key'].blank?
|
111
|
+
result['loc-key'] = alert['loc-key'] unless alert['loc-key'].blank?
|
112
|
+
unless alert['loc-args'].blank?
|
113
|
+
if alert['loc-args'].is_a? Hash
|
114
|
+
result['loc-args'] = Array.new(alert['loc-args'].size)
|
115
|
+
alert['loc-args'].map do |key,value|
|
116
|
+
result['loc-args'][key.to_i] = value
|
117
|
+
end
|
118
|
+
else
|
119
|
+
result['loc-args'] = alert['loc-args']
|
120
|
+
end
|
117
121
|
end
|
122
|
+
return result
|
118
123
|
else
|
119
|
-
|
124
|
+
return alert
|
120
125
|
end
|
121
126
|
end
|
122
|
-
return result
|
123
|
-
else
|
124
|
-
return alert
|
125
|
-
end
|
126
|
-
end
|
127
127
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
128
|
+
#
|
129
|
+
# Generate a JSON string from the given Hash/Array which does not screw up the ordering of the Hash
|
130
|
+
#
|
131
|
+
def self.to_json(hash)
|
132
|
+
if hash.is_a? Hash
|
133
|
+
hash_keys = hash.keys
|
134
134
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
135
|
+
result = '{'
|
136
|
+
result << hash_keys.map do |key|
|
137
|
+
if hash[key].is_a?(Hash) || hash[key].is_a?(Array)
|
138
|
+
"#{key.to_s.to_json}:#{to_json(hash[key])}"
|
139
|
+
else
|
140
|
+
"#{key.to_s.to_json}:#{hash[key].to_json}"
|
141
|
+
end
|
142
|
+
end * ','
|
143
|
+
result << '}'
|
144
|
+
elsif hash.is_a? Array
|
145
|
+
result = '['
|
146
|
+
result << hash.map do |value|
|
147
|
+
if value.is_a?(Hash) || value.is_a?(Array)
|
148
|
+
"#{to_json(value)}"
|
149
|
+
else
|
150
|
+
value.to_json
|
151
|
+
end
|
152
|
+
end * ','
|
153
|
+
result << ']'
|
141
154
|
end
|
142
|
-
end
|
143
|
-
result << '}'
|
144
|
-
elsif hash.is_a? Array
|
145
|
-
result = '['
|
146
|
-
result << hash.map do |value|
|
147
|
-
if value.is_a?(Hash) || value.is_a?(Array)
|
148
|
-
"#{to_json(value)}"
|
149
|
-
else
|
150
|
-
value.to_json
|
151
|
-
end
|
152
|
-
end * ','
|
153
|
-
result << ']'
|
154
|
-
end
|
155
|
-
end
|
155
|
+
end
|
156
156
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
157
|
+
#
|
158
|
+
# The message formatted for sending in binary
|
159
|
+
#
|
160
|
+
def formatted
|
161
|
+
Resque::Plugins::Aps::Notification.format_message_for_sending(self.device_token, self.payload, self.batch_id, self.expiry)
|
162
|
+
end
|
163
163
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
164
|
+
#
|
165
|
+
# A HEX dump of the formatted message so that you can debug the binary data
|
166
|
+
#
|
167
|
+
def to_hex
|
168
|
+
formatted.unpack('H*')
|
169
|
+
end
|
170
170
|
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
171
|
+
#
|
172
|
+
# HEX version of the device token
|
173
|
+
#
|
174
|
+
def self.device_token_hex(device_token)
|
175
|
+
#self.device_token
|
176
|
+
[device_token.gsub(' ', '')].pack('H*')
|
177
|
+
end
|
178
178
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
179
|
+
#
|
180
|
+
# Combine the device token and JSON into a binary package to send.
|
181
|
+
#
|
182
|
+
def self.format_message_for_sending(device_token, json, batch_id = nil, expiry = nil)
|
183
|
+
token_hex = self.device_token_hex(device_token)
|
184
|
+
tl = [token_hex.length].pack('n')
|
185
|
+
# puts("token length [#{tl.unpack('H*')}]")
|
186
|
+
# puts("device token [#{token_hex.unpack('H*')}]")
|
187
|
+
# logger.debug "Formatting #{json} for #{self.device_token}"
|
188
|
+
jl = [json.length].pack('n')
|
189
|
+
# puts("json length [#{jl.unpack('H*')}]")
|
190
|
+
# puts("json [#{json}]")
|
191
|
+
unless batch_id
|
192
|
+
"\0#{tl}#{token_hex}#{jl}#{json}"
|
193
|
+
# "\0\0 #{token_hex}\0#{json.length.chr}#{json}"
|
194
|
+
else
|
195
|
+
bid = [batch_id].pack('N')
|
196
|
+
exp = [expiry].pack('N')
|
197
|
+
"\1#{bid}#{exp}#{tl}#{token_hex}#{jl}#{json}"
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
195
202
|
end
|
196
|
-
end
|
197
203
|
end
|
data/lib/resque_aps/version.rb
CHANGED
data/lib/resque_aps.rb
CHANGED
@@ -14,126 +14,146 @@ module Resque
|
|
14
14
|
module Plugins
|
15
15
|
module Aps
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
17
|
+
def logger=(logger)
|
18
|
+
@logger = logger
|
19
|
+
end
|
20
|
+
|
21
|
+
def logger
|
22
|
+
unless @logger
|
23
|
+
@logger = Logger.new(STDOUT)
|
24
|
+
@logger.level = Logger::WARN
|
25
|
+
end
|
26
|
+
@logger
|
27
|
+
end
|
28
|
+
|
29
|
+
def aps_gateway_host=(host)
|
30
|
+
@aps_gateway_host = host
|
31
|
+
end
|
32
|
+
|
33
|
+
def aps_gateway_host
|
34
|
+
@aps_gateway_host ||= "gateway.sandbox.push.apple.com"
|
35
|
+
end
|
36
|
+
|
37
|
+
def aps_gateway_port=(port)
|
38
|
+
@aps_gateway_port = port
|
39
|
+
end
|
40
|
+
|
41
|
+
def aps_gateway_port
|
42
|
+
@aps_gateway_port ||= 2195
|
43
|
+
end
|
44
|
+
|
45
|
+
def aps_feedback_host=(host)
|
46
|
+
@aps_feedback_host = host
|
47
|
+
end
|
48
|
+
|
49
|
+
def aps_feedback_host
|
50
|
+
@aps_feedback_host ||= "feedback.sandbox.push.apple.com"
|
51
|
+
end
|
52
|
+
|
53
|
+
def aps_feedback_port=(port)
|
54
|
+
@aps_feedback_port = port
|
55
|
+
end
|
56
|
+
|
57
|
+
def aps_feedback_port
|
58
|
+
@aps_feedback_port ||= 2196
|
59
|
+
end
|
60
|
+
|
61
|
+
def aps_queue_size_upper=(size)
|
62
|
+
@aps_queue_size_upper = size
|
63
|
+
end
|
64
|
+
|
65
|
+
def aps_queue_size_upper
|
66
|
+
@aps_queue_size_upper ||= 1000
|
67
|
+
end
|
68
|
+
|
69
|
+
def aps_application_job_limit=(size)
|
70
|
+
@aps_queue_size_upper = size
|
71
|
+
end
|
72
|
+
|
73
|
+
def aps_application_job_limit
|
74
|
+
@aps_application_job_limit ||= 5
|
75
|
+
end
|
76
|
+
|
77
|
+
def aps_applications_queued_count(application_name)
|
78
|
+
redis.get(aps_application_queued_key(application_name)) || 0
|
79
|
+
end
|
80
|
+
|
81
|
+
def enqueue_aps_application(application_name, override = false)
|
82
|
+
count_apps = aps_applications_queued_count(application_name)
|
83
|
+
count_not = aps_notification_count_for_application(application_name)
|
84
|
+
if override || count_apps <= 0 || (count_apps < aps_application_job_limit && (count_not > aps_queue_size_upper && count_not % (aps_queue_size_upper / 10) == 0))
|
85
|
+
enqueue(Resque::Plugins::Aps::Application, application_name)
|
86
|
+
redis.incr(aps_application_queued_key(application_name))
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def dequeue_aps_application(application_name)
|
91
|
+
redis.decr(aps_application_queued_key(application_name)) if aps_applications_queued_count(application_name) > 0
|
92
|
+
end
|
93
|
+
|
94
|
+
def enqueue_aps(application_name, notification)
|
95
|
+
redis.rpush(aps_application_queue_key(application_name), encode(notification.to_hash))
|
96
|
+
enqueue_aps_application(application_name)
|
97
|
+
true
|
98
|
+
end
|
83
99
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
100
|
+
def dequeue_aps(application_name)
|
101
|
+
h = decode(redis.lpop(aps_application_queue_key(application_name)))
|
102
|
+
return Resque::Plugins::Aps::Notification.new(h) if h
|
103
|
+
nil
|
104
|
+
end
|
105
|
+
|
106
|
+
# Returns the number of queued notifications for a given application
|
107
|
+
def aps_notification_count_for_application(application_name)
|
108
|
+
redis.llen(aps_application_queue_key(application_name)).to_i
|
109
|
+
end
|
94
110
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
111
|
+
# Returns an array of queued notifications for the given application
|
112
|
+
def aps_notifications_for_application(application_name, start = 0, count = 1)
|
113
|
+
r = redis.lrange(aps_application_queue_key(application_name), start, count)
|
114
|
+
if r
|
115
|
+
r.map { |h| Resque::Plugins::Aps::Notification.new(decode(h)) }
|
116
|
+
else
|
117
|
+
[]
|
118
|
+
end
|
119
|
+
end
|
104
120
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
121
|
+
def create_aps_application(name, cert_file, cert_passwd = nil)
|
122
|
+
redis.set(aps_application_key(name), encode({'name' => name, 'cert_file' => cert_file, 'cert_passwd' => cert_passwd}))
|
123
|
+
redis.sadd(:aps_applications, name)
|
124
|
+
end
|
125
|
+
|
126
|
+
def aps_application(name)
|
127
|
+
h = decode(redis.get(aps_application_key(name)))
|
128
|
+
return Resque::Plugins::Aps::Application.new(h) if h
|
129
|
+
nil
|
130
|
+
end
|
131
|
+
|
132
|
+
# Returns an array of applications based on start and count
|
133
|
+
def aps_application_names(start = 0, count = 1)
|
134
|
+
a = redis.smembers(:aps_applications)
|
135
|
+
return a if count == 0
|
136
|
+
ret = a[start..(start + count)]
|
137
|
+
return [] unless ret
|
138
|
+
ret
|
139
|
+
end
|
124
140
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
141
|
+
# Returns the number of application queues
|
142
|
+
def aps_applications_count
|
143
|
+
redis.smembers(:aps_applications).size
|
144
|
+
end
|
129
145
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
146
|
+
def aps_application_key(application_name)
|
147
|
+
"aps:application:#{application_name}"
|
148
|
+
end
|
149
|
+
|
150
|
+
def aps_application_queued_key(application_name)
|
151
|
+
"#{aps_application_key(application_name)}:queued"
|
152
|
+
end
|
153
|
+
|
154
|
+
def aps_application_queue_key(application_name)
|
155
|
+
"#{aps_application_key(application_name)}:queue"
|
156
|
+
end
|
137
157
|
end
|
138
158
|
end
|
139
159
|
end
|
metadata
CHANGED
@@ -1,12 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: resque-aps
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
5
|
-
segments:
|
6
|
-
- 0
|
7
|
-
- 9
|
8
|
-
- 9
|
9
|
-
version: 0.9.9
|
4
|
+
version: 0.9.10
|
10
5
|
platform: ruby
|
11
6
|
authors:
|
12
7
|
- Ashley Martens
|
@@ -14,73 +9,59 @@ autorequire:
|
|
14
9
|
bindir: bin
|
15
10
|
cert_chain: []
|
16
11
|
|
17
|
-
date: 2010-08-
|
12
|
+
date: 2010-08-31 00:00:00 -07:00
|
18
13
|
default_executable:
|
19
14
|
dependencies:
|
20
15
|
- !ruby/object:Gem::Dependency
|
21
16
|
name: redis
|
22
|
-
|
23
|
-
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
20
|
requirements:
|
25
21
|
- - ">="
|
26
22
|
- !ruby/object:Gem::Version
|
27
|
-
segments:
|
28
|
-
- 1
|
29
|
-
- 0
|
30
|
-
- 7
|
31
23
|
version: 1.0.7
|
32
|
-
|
33
|
-
version_requirements: *id001
|
24
|
+
version:
|
34
25
|
- !ruby/object:Gem::Dependency
|
35
26
|
name: resque
|
36
|
-
|
37
|
-
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
30
|
requirements:
|
39
31
|
- - ">="
|
40
32
|
- !ruby/object:Gem::Version
|
41
|
-
segments:
|
42
|
-
- 1
|
43
|
-
- 5
|
44
|
-
- 0
|
45
33
|
version: 1.5.0
|
46
|
-
|
47
|
-
version_requirements: *id002
|
34
|
+
version:
|
48
35
|
- !ruby/object:Gem::Dependency
|
49
36
|
name: jeweler
|
50
|
-
|
51
|
-
|
37
|
+
type: :development
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
40
|
requirements:
|
53
41
|
- - ">="
|
54
42
|
- !ruby/object:Gem::Version
|
55
|
-
segments:
|
56
|
-
- 0
|
57
43
|
version: "0"
|
58
|
-
|
59
|
-
version_requirements: *id003
|
44
|
+
version:
|
60
45
|
- !ruby/object:Gem::Dependency
|
61
46
|
name: mocha
|
62
|
-
|
63
|
-
|
47
|
+
type: :development
|
48
|
+
version_requirement:
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
50
|
requirements:
|
65
51
|
- - ">="
|
66
52
|
- !ruby/object:Gem::Version
|
67
|
-
segments:
|
68
|
-
- 0
|
69
53
|
version: "0"
|
70
|
-
|
71
|
-
version_requirements: *id004
|
54
|
+
version:
|
72
55
|
- !ruby/object:Gem::Dependency
|
73
56
|
name: rack-test
|
74
|
-
|
75
|
-
|
57
|
+
type: :development
|
58
|
+
version_requirement:
|
59
|
+
version_requirements: !ruby/object:Gem::Requirement
|
76
60
|
requirements:
|
77
61
|
- - ">="
|
78
62
|
- !ruby/object:Gem::Version
|
79
|
-
segments:
|
80
|
-
- 0
|
81
63
|
version: "0"
|
82
|
-
|
83
|
-
version_requirements: *id005
|
64
|
+
version:
|
84
65
|
description: |-
|
85
66
|
Queuing system for Apple's Push Service on top of Resque.
|
86
67
|
Adds methods enqueue_aps to queue a notification message.
|
@@ -132,20 +113,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
132
113
|
requirements:
|
133
114
|
- - ">="
|
134
115
|
- !ruby/object:Gem::Version
|
135
|
-
segments:
|
136
|
-
- 0
|
137
116
|
version: "0"
|
117
|
+
version:
|
138
118
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
139
119
|
requirements:
|
140
120
|
- - ">="
|
141
121
|
- !ruby/object:Gem::Version
|
142
|
-
segments:
|
143
|
-
- 0
|
144
122
|
version: "0"
|
123
|
+
version:
|
145
124
|
requirements: []
|
146
125
|
|
147
126
|
rubyforge_project:
|
148
|
-
rubygems_version: 1.3.
|
127
|
+
rubygems_version: 1.3.5
|
149
128
|
signing_key:
|
150
129
|
specification_version: 3
|
151
130
|
summary: Queuing system for Apple's Push Service on top of Resque
|