Push0r 0.4.4 → 0.5.0
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.
- checksums.yaml +4 -4
- data/lib/push0r/APNS/ApnsPushMessage.rb +38 -34
- data/lib/push0r/APNS/ApnsService.rb +231 -228
- data/lib/push0r/FlushResult.rb +46 -46
- data/lib/push0r/GCM/GcmPushMessage.rb +24 -24
- data/lib/push0r/GCM/GcmService.rb +126 -124
- data/lib/push0r/PushMessage.rb +27 -27
- data/lib/push0r/Queue.rb +74 -74
- data/lib/push0r/Service.rb +42 -42
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6fff2260692ade292cbbc87b6f9df4fc214dabda
|
4
|
+
data.tar.gz: 88dc9988dafff0da09a52b3e58bf4fd69bf36235
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 00aba3ca41d749b4c937f94cb44f1c871fba600eadc37a99a99974a2118e0fa944acf331c436029d46f918af6744266be80b9fb2509a1e51e8e6a30871ae9001
|
7
|
+
data.tar.gz: 7025b1127c3e619495803289416290cd28f67518260ef8bdd0b3c769720edd83ec6db52f46753954636f733f5056c63b17bff7cf2c360498181a28a243640843
|
@@ -1,40 +1,44 @@
|
|
1
1
|
module Push0r
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
2
|
+
# ApnsPushMessage is a {PushMessage} implementation that encapsulates a single push notification to be sent to a single user.
|
3
|
+
class ApnsPushMessage < PushMessage
|
4
|
+
attr_reader :environment
|
5
|
+
|
6
|
+
# Returns a new ApnsPushMessage instance that encapsulates a single push notification to be sent to a single user.
|
7
|
+
# @param receiver_token [String] the apns push token (aka device token) to push the notification to
|
8
|
+
# @param environment [Fixnum] the environment to use when sending this push message. Defaults to ApnsEnvironment::PRODUCTION.
|
9
|
+
# @param identifier [Fixnum] a unique identifier to identify this push message during error handling. If nil, a random identifier is automatically generated.
|
10
|
+
# @param time_to_live [Fixnum] The time to live in seconds for this push messages. If nil, the time to live is set to zero seconds.
|
11
|
+
def initialize(receiver_token, environment = ApnsEnvironment::PRODUCTION, identifier = nil, time_to_live = nil)
|
12
|
+
if identifier.nil? ## make sure the message has an identifier (required for apns error handling)
|
13
|
+
identifier = Random.rand(2**32)
|
14
|
+
end
|
15
|
+
super(receiver_token, identifier, time_to_live)
|
16
|
+
@environment = environment
|
17
|
+
end
|
18
|
+
|
19
|
+
# Convenience method to attach common data (that is an alert, a sound or a badge value) to this message's payload.
|
20
|
+
# @param alert_text [String] the alert text to be displayed
|
21
|
+
# @param sound [String] the sound to be played
|
22
|
+
# @param badge [Fixnum] the badge value to be displayed
|
20
23
|
# @param category [String] the category this message belongs to (see UIUserNotificationCategory in apple's documentation)
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
24
|
+
def simple(alert_text = nil, sound = nil, badge = nil, category = nil)
|
25
|
+
new_payload = {aps: {}}
|
26
|
+
if alert_text
|
27
|
+
new_payload[:aps][:alert] = alert_text
|
28
|
+
end
|
29
|
+
if sound
|
30
|
+
new_payload[:aps][:sound] = sound
|
31
|
+
end
|
32
|
+
if badge
|
33
|
+
new_payload[:aps][:badge] = badge
|
34
|
+
end
|
32
35
|
if category
|
33
36
|
new_payload[:aps][:category] = category
|
34
37
|
end
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
38
|
+
|
39
|
+
@payload.merge!(new_payload)
|
40
|
+
|
41
|
+
return self
|
42
|
+
end
|
43
|
+
end
|
40
44
|
end
|
@@ -1,231 +1,234 @@
|
|
1
1
|
module Push0r
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
3
|
+
# A module that contains Apple Push Notification Service error codes
|
4
|
+
module ApnsErrorCodes
|
5
|
+
PROCESSING_ERROR = 1
|
6
|
+
MISSING_DEVICE_TOKEN = 2
|
7
|
+
MISSING_TOPIC = 3
|
8
|
+
MISSING_PAYLOAD = 4
|
9
|
+
INVALID_TOKEN_SIZE = 5
|
10
|
+
INVALID_TOPIC_SIZE = 6
|
11
|
+
INVALID_PAYLOAD_SIZE = 7
|
12
|
+
INVALID_TOKEN = 8
|
13
|
+
SHUTDOWN = 10
|
14
|
+
NONE = 255
|
15
|
+
end
|
16
|
+
|
17
|
+
module ApnsEnvironment
|
18
|
+
PRODUCTION = 0
|
19
|
+
SANDBOX = 1
|
20
|
+
end
|
21
|
+
|
22
|
+
# ApnsService is a {Service} implementation to push notifications to iOS and OSX users using the Apple Push Notification Service.
|
23
|
+
# @example
|
24
|
+
# queue = Push0r::Queue.new
|
25
|
+
#
|
26
|
+
# apns_service = Push0r::ApnsService.new(File.read("aps.pem"), Push0r::ApnsEnvironment::SANDBOX)
|
27
|
+
# queue.register_service(apns_service)
|
28
|
+
class ApnsService < Service
|
29
|
+
|
30
|
+
# Returns a new ApnsService instance
|
31
|
+
# @param certificate_data [String] the Apple push certificate in PEM format
|
32
|
+
# @param environment [Fixnum] the environment to use when sending messages. Either ApnsEnvironment::PRODUCTION or ApnsEnvironment::SANDBOX. Defaults to ApnsEnvironment::PRODUCTION.
|
33
|
+
def initialize(certificate_data, environment = ApnsEnvironment::PRODUCTION)
|
34
|
+
@certificate_data = certificate_data
|
35
|
+
@environment = environment
|
36
|
+
@ssl = nil
|
37
|
+
@sock = nil
|
38
|
+
@messages = []
|
39
|
+
end
|
40
|
+
|
41
|
+
# @see Service#can_send?
|
42
|
+
def can_send?(message)
|
43
|
+
return message.is_a?(ApnsPushMessage) && message.environment == @environment
|
44
|
+
end
|
45
|
+
|
46
|
+
# @see Service#send
|
47
|
+
def send(message)
|
48
|
+
@messages << message
|
49
|
+
end
|
50
|
+
|
51
|
+
# @see Service#init_push
|
52
|
+
def init_push
|
53
|
+
# not used for apns
|
54
|
+
end
|
55
|
+
|
56
|
+
# @see Service#end_push
|
57
|
+
def end_push
|
58
|
+
failed_messages = []
|
59
|
+
result = false
|
60
|
+
begin
|
61
|
+
begin
|
62
|
+
setup_ssl(true)
|
63
|
+
rescue SocketError => e
|
64
|
+
puts "Error: #{e}"
|
65
|
+
break
|
66
|
+
end
|
67
|
+
(result, error_message, error_code) = transmit_messages
|
68
|
+
unless result
|
69
|
+
failed_messages << FailedMessage.new(error_code, [error_message.receiver_token], error_message)
|
70
|
+
reset_message(error_message.identifier)
|
71
|
+
result = true if @messages.empty?
|
72
|
+
end
|
73
|
+
end until result
|
74
|
+
|
75
|
+
close_ssl
|
76
|
+
|
77
|
+
@messages = [] ## reset
|
78
|
+
return [failed_messages, []]
|
79
|
+
end
|
80
|
+
|
81
|
+
# Calls the APNS feedback service and returns an array of expired push tokens
|
82
|
+
# @return [Array<String>] an array of expired push tokens
|
83
|
+
def get_feedback
|
84
|
+
tokens = []
|
85
|
+
|
86
|
+
begin
|
87
|
+
setup_ssl(true)
|
88
|
+
rescue SocketError => e
|
89
|
+
puts "Error: #{e}"
|
90
|
+
return tokens
|
91
|
+
end
|
92
|
+
|
93
|
+
if IO.select([@ssl], nil, nil, 1)
|
94
|
+
while (line = @ssl.read(38))
|
95
|
+
f = line.unpack('N1n1H64')
|
96
|
+
time = Time.at(f[0])
|
97
|
+
token = f[2].scan(/.{8}/).join(' ')
|
98
|
+
tokens << token
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
close_ssl
|
103
|
+
|
104
|
+
return tokens
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
def setup_ssl(for_feedback = false)
|
109
|
+
close_ssl
|
110
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
111
|
+
|
112
|
+
ctx.key = OpenSSL::PKey::RSA.new(@certificate_data, '')
|
113
|
+
ctx.cert = OpenSSL::X509::Certificate.new(@certificate_data)
|
114
|
+
|
115
|
+
@sock = nil
|
116
|
+
unless for_feedback
|
117
|
+
@sock = TCPSocket.new(@environment == ApnsEnvironment::SANDBOX ? 'gateway.sandbox.push.apple.com' : 'gateway.push.apple.com', 2195)
|
118
|
+
else
|
119
|
+
@sock = TCPSocket.new(@environment == ApnsEnvironment::SANDBOX ? 'feedback.sandbox.push.apple.com' : 'feedback.push.apple.com', 2195)
|
120
|
+
end
|
121
|
+
@ssl = OpenSSL::SSL::SSLSocket.new(@sock, ctx)
|
122
|
+
@ssl.connect
|
123
|
+
end
|
124
|
+
|
125
|
+
def close_ssl
|
126
|
+
if !@ssl.nil? && !@ssl.closed?
|
127
|
+
begin
|
128
|
+
@ssl.close
|
129
|
+
rescue IOError
|
130
|
+
end
|
131
|
+
end
|
132
|
+
@ssl = nil
|
133
|
+
|
134
|
+
if !@sock.nil? && !@sock.closed?
|
135
|
+
begin
|
136
|
+
@sock.close
|
137
|
+
rescue IOError
|
138
|
+
end
|
139
|
+
end
|
140
|
+
@sock = nil
|
141
|
+
end
|
142
|
+
|
143
|
+
def reset_message(error_identifier)
|
144
|
+
index = @messages.find_index { |o| o.identifier == error_identifier }
|
145
|
+
|
146
|
+
if index.nil? ## this should never happen actually
|
147
|
+
@messages = []
|
148
|
+
elsif index < @messages.length - 1 # reset @messages to contain all messages after the one that has failed
|
149
|
+
@messages = @messages[index+1, @messages.length]
|
150
|
+
else ## the very last message failed, so there's nothing left to be sent
|
151
|
+
@messages = []
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def create_push_frame(message)
|
156
|
+
receiver_token = message.receiver_token
|
157
|
+
payload = message.payload
|
158
|
+
identifier = message.identifier
|
159
|
+
time_to_live = (message.time_to_live.nil? || message.time_to_live.to_i < 0) ? 0 : message.time_to_live.to_i
|
160
|
+
|
161
|
+
raise(ArgumentError, 'receiver_token is nil!') if receiver_token.nil?
|
162
|
+
|
163
|
+
raise(ArgumentError, 'payload is nil!') if payload.nil?
|
164
|
+
|
165
|
+
receiver_token = receiver_token.gsub(/\s+/, '')
|
166
|
+
raise(ArgumentError, 'invalid receiver_token length!') if receiver_token.length != 64
|
167
|
+
|
168
|
+
devicetoken = [receiver_token].pack('H*')
|
169
|
+
devicetoken_length = [32].pack('n')
|
170
|
+
devicetoken_item = "\1#{devicetoken_length}#{devicetoken}"
|
171
|
+
|
172
|
+
identifier = [identifier.to_i].pack('N')
|
173
|
+
identifier_length = [4].pack('n')
|
174
|
+
identifier_item = "\3#{identifier_length}#{identifier}"
|
175
|
+
|
176
|
+
expiration_date = [(time_to_live > 0 ? Time.now.to_i + time_to_live : 0)].pack('N')
|
177
|
+
expiration_date_length = [4].pack('n')
|
178
|
+
expiration_item = "\4#{expiration_date_length}#{expiration_date}"
|
179
|
+
|
180
|
+
priority = "\xA" ## default: high priority
|
181
|
+
if payload[:aps] && payload[:aps]['content-available'] && payload[:aps]['content-available'].to_i != 0 && (payload[:aps][:alert].nil? && payload[:aps][:sound].nil? && payload[:aps][:badge].nil?)
|
182
|
+
priority = "\5" ## lower priority for content-available pushes without alert/sound/badge
|
183
|
+
end
|
184
|
+
|
185
|
+
priority_length = [1].pack('n')
|
186
|
+
priority_item = "\5#{priority_length}#{priority}"
|
187
|
+
|
188
|
+
payload = payload.to_json.force_encoding('BINARY')
|
189
|
+
payload_length = [payload.bytesize].pack('n')
|
190
|
+
payload_item = "\2#{payload_length}#{payload}"
|
191
|
+
|
192
|
+
frame_length = [devicetoken_item.bytesize + payload_item.bytesize + identifier_item.bytesize + expiration_item.bytesize + priority_item.bytesize].pack('N')
|
193
|
+
frame = "\2#{frame_length}#{devicetoken_item}#{payload_item}#{identifier_item}#{expiration_item}#{priority_item}"
|
194
|
+
|
195
|
+
return frame
|
196
|
+
end
|
197
|
+
|
198
|
+
def transmit_messages
|
199
|
+
if @messages.empty? || @ssl.nil?
|
200
|
+
return [true, nil, nil]
|
201
|
+
end
|
202
|
+
|
203
|
+
pushdata = ''
|
204
|
+
@messages.each do |message|
|
205
|
+
pushdata << create_push_frame(message)
|
206
|
+
end
|
207
|
+
|
208
|
+
@ssl.write(pushdata)
|
209
|
+
|
210
|
+
if IO.select([@ssl], nil, nil, 1)
|
211
|
+
begin
|
212
|
+
read_buffer = @ssl.read(6)
|
213
|
+
rescue Exception
|
214
|
+
return [true, nil, nil]
|
215
|
+
end
|
216
|
+
if !read_buffer.nil?
|
217
|
+
#cmd = read_buffer[0].unpack("C").first
|
218
|
+
error_code = read_buffer[1].unpack('C').first
|
219
|
+
identifier = read_buffer[2, 4].unpack('N').first
|
220
|
+
puts "ERROR: APNS returned error code #{error_code} #{identifier}"
|
221
|
+
return [false, message_for_identifier(identifier), error_code]
|
222
|
+
else
|
223
|
+
return [true, nil, nil]
|
224
|
+
end
|
225
|
+
end
|
226
|
+
return [true, nil, nil]
|
227
|
+
end
|
228
|
+
|
229
|
+
def message_for_identifier(identifier)
|
230
|
+
index = @messages.find_index { |o| o.identifier == identifier }
|
231
|
+
index.nil? ? nil : @messages[index]
|
232
|
+
end
|
233
|
+
end
|
231
234
|
end
|