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 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