Push0r 0.4.4 → 0.5.0

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