gs-apns 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ .DS_Store
2
+ pkg
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in APNS.gemspec
4
+ gemspec
data/MIT-LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2009 James Pozdena, 2010 Justin.tv, 2013 Geospike Pty Ltd
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
data/README.textile ADDED
@@ -0,0 +1,158 @@
1
+ h1. gs-apns
2
+
3
+ gs-apns is a gem for accessing the Apple Push Notification Service that allows
4
+ both sending notifications and reading from apple's feedback service. This gem
5
+ is based heavily on the work of James Pozdena (http://github.com/jpoz/APNS) and
6
+ "Paul Gebheim" (http://github.com/jpoz/APNS).
7
+
8
+ Updates by William Denniss of Geospike: Apple's APNS service does not support JSON that isn't ascii encoded. Prior to rails 3.2.13
9
+ it was encoded, but now it isn't.
10
+
11
+
12
+ h2. Install
13
+
14
+ <pre>
15
+ sudo gem install jtv-apns
16
+ </pre>
17
+
18
+ h2. Setup:
19
+
20
+ First, you will need to export your development/production iphone push service
21
+ certificate and key from your keychain. To do this select the private key and
22
+ certificate, and select File -> Export from your keychain. By default a .p12
23
+ file will be generated containing certificate and key.
24
+
25
+ Next, run the following command to convert this .p12 file into a .pem file.
26
+ <pre>
27
+ openssl pkcs12 -in cert.p12 -out cert.pem -nodes -clcerts
28
+ </pre>
29
+
30
+ This pem file should be stored somewhere secure that your application can access. Next, set the jtv-apns configuration parameters:
31
+
32
+ <pre>
33
+ ###################
34
+ # Hosts Config
35
+ ###################
36
+
37
+ # Push Notification Service:
38
+ #
39
+ # (default: gateway.sandbox.push.apple.com is)
40
+ # Set as below for a production install
41
+ APNS.host = 'gateway.push.apple.com'
42
+
43
+ # (default: 2195)
44
+ # APNS.port = 2195
45
+
46
+ # Feedback Service:
47
+ #
48
+ # (default: feedback.sandbox.push.apple.com)
49
+ APNS.feedback_host = 'feedback.push.apple.com'
50
+
51
+ # (default: 2196)
52
+ # APNS.feedback_port = 2196
53
+
54
+ ####################
55
+ # Certificate Setup
56
+ ####################
57
+
58
+ # Path to the .pem file created earlier
59
+ APNS.pem = '/path/to/pem/file'
60
+
61
+ # Password for decrypting the .pem file, if one was used
62
+ APNS.pass = 'xxxx'
63
+
64
+ ####################
65
+ # Connection Mgmt
66
+ ####################
67
+
68
+ # Cache open connections when sending push notifications
69
+ # this will force the gem to keep 1 connection open per
70
+ # host/port pair, and reuse it when sending notifications
71
+
72
+ # (default: false)
73
+ # APNS.cache_connections = true
74
+ </pre>
75
+
76
+ h2. Example (Single notification):
77
+
78
+ Then to send a push notification you can either just send a string as the alert or give it a hash for the alert, badge and sound.
79
+
80
+ <pre>
81
+ device_token = '123abc456def'
82
+
83
+ APNS.send_notification(device_token, 'Hello iPhone!')
84
+ APNS.send_notification(device_token, :aps => {:alert => 'Hello iPhone!', :badge => 1, :sound => 'default'})
85
+ </pre>
86
+
87
+ h2. Example (Multiple notifications):
88
+
89
+ You can also send multiple notifications using the same connection to Apple:
90
+
91
+ <pre>
92
+ device_token = '123abc456def'
93
+
94
+ n1 = [device_token, :aps => { :alert => 'Hello...', :badge => 1, :sound => 'default' }
95
+ n2 = [device_token, :aps => { :alert => '... iPhone!', :badge => 1, :sound => 'default' }]
96
+
97
+ APNS.send_notifications([n1, n2])
98
+ </pre>
99
+
100
+
101
+ h2. Send other info along with aps
102
+
103
+ Application-specific information can be included along with the alert by
104
+ passing it along with the "aps" key in the message hash.
105
+
106
+ <pre>
107
+ APNS.send_notification(device_token, :aps => { :alert => 'Hello iPhone!', :badge => 1, :sound => 'default'},
108
+ :sent_by => 'Justin.tv')
109
+ </pre>
110
+
111
+ h2. Pre-establishing connections
112
+
113
+ If connection caching is enabled, you can tell the gem to establish connections before sending any notifications.
114
+
115
+ <pre>
116
+ APNS.establish_notification_connection
117
+ # ...
118
+ if APNS.has_notification_connection?
119
+ APNS.send_notification(device_token, "It works!")
120
+ end
121
+ </pre>
122
+
123
+ h2. Accessing the feedback service
124
+
125
+ jtv-apns provides a simple api to access Apple's feedback service. Below is an example for setting the feedback time on an ActiveRecord object corresponding to a device token.
126
+
127
+ <pre>
128
+ # APNS.feedback_each returns an array of Hash objects with the following keys
129
+ # :feedback_on => (Time) Time Apple considers app unregistered from device
130
+ # :length => (Fixnum) Length of :device_token, currently always 32 (bytes)
131
+ # :device_token => (String) hex-encoded device token
132
+ APNS.feedback.each do |feedback|
133
+ d = RegisteredDevices.find(:first, :conditions => { :device_token = feedback.device_token })
134
+ unless d.nil?
135
+ d.feedback_on = feedback.feedback_on
136
+ end
137
+ end
138
+ </pre>
139
+
140
+ h2. Getting your iPhone's device token
141
+
142
+ After you setup push notification for your application with Apple. You need to ask Apple for you application specific device token.
143
+
144
+ ApplicationAppDelegate.m
145
+ <pre>
146
+ - (void)applicationDidFinishLaunching:(UIApplication *)application {
147
+ // Register with apple that this app will use push notification
148
+ [[UIApplication sharedApplication] registerForRemoteNotificationTypes:(UIRemoteNotificationTypeAlert |
149
+ UIRemoteNotificationTypeSound | UIRemoteNotificationTypeBadge)];
150
+ }
151
+
152
+ - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
153
+ // Show the device token obtained from apple to the log
154
+ NSLog(@"deviceToken: %@", deviceToken);
155
+ }
156
+ </pre>
157
+
158
+
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/apns.gemspec ADDED
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/apns/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["William Denniss", "Paul Gebheim", "James Pozdena"]
6
+ gem.email = ["will@geospike.com"]
7
+ gem.description = %q{Simple Apple push notification service gem}
8
+ gem.summary = %q{Simple Apple push notification service gem}
9
+ gem.homepage = %q{http://github.com/WilliamDenniss/APNS}
10
+
11
+ gem.extra_rdoc_files = ["MIT-LICENSE"]
12
+ gem.files = `git ls-files`.split($\)
13
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
14
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
15
+ gem.name = "gs-apns"
16
+ gem.require_paths = ["lib"]
17
+ gem.version = APNS::VERSION
18
+
19
+ gem.add_development_dependency "rspec", "~> 2.6"
20
+ end
data/lib/apns.rb ADDED
@@ -0,0 +1,27 @@
1
+ # Copyright (c) 2009 James Pozdena, 2010 Justin.tv
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person
4
+ # obtaining a copy of this software and associated documentation
5
+ # files (the "Software"), to deal in the Software without
6
+ # restriction, including without limitation the rights to use,
7
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the
9
+ # Software is furnished to do so, subject to the following
10
+ # conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ # OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ require 'apns/exceptions'
25
+ require 'apns/apns_json'
26
+ require 'apns/truncate'
27
+ require 'apns/core'
@@ -0,0 +1,47 @@
1
+ # Copyright (c) 2013 William Denniss
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person
4
+ # obtaining a copy of this software and associated documentation
5
+ # files (the "Software"), to deal in the Software without
6
+ # restriction, including without limitation the rights to use,
7
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the
9
+ # Software is furnished to do so, subject to the following
10
+ # conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ # OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ module APNS
25
+ require 'json'
26
+
27
+ class ApnsJSON
28
+
29
+ # generates JSON in a format acceptable to the APNS service (which is a subset of the JSON standard)
30
+ def self.apns_json(object)
31
+
32
+ JSON.generate(object, :ascii_only => false)
33
+ end
34
+
35
+ # calculates the byte-length of an object when encoded with APNS friendly JSON encoding
36
+ # if a string is passed, the byte-size is calculated as if it were in an object structure
37
+ def self.apns_json_size(object)
38
+
39
+ if object.is_a?(Hash) || object.is_a?(Array)
40
+ return apns_json(object).bytesize
41
+ else object.is_a?(String)
42
+ # wraps string in an array but discounts the extra chars
43
+ return apns_json([object]).bytesize - 4
44
+ end
45
+ end
46
+ end
47
+ end
data/lib/apns/core.rb ADDED
@@ -0,0 +1,254 @@
1
+ # Copyright (c) 2009 James Pozdena, 2010 Justin.tv, 2013 William Denniss
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person
4
+ # obtaining a copy of this software and associated documentation
5
+ # files (the "Software"), to deal in the Software without
6
+ # restriction, including without limitation the rights to use,
7
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the
9
+ # Software is furnished to do so, subject to the following
10
+ # conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ # OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ module APNS
25
+ require 'socket'
26
+ require 'openssl'
27
+
28
+ # Host for push notification service
29
+ # production: gateway.push.apple.com
30
+ # development: gateway.sandbox.apple.com
31
+ @host = 'gateway.sandbox.push.apple.com'
32
+ @port = 2195
33
+
34
+ # Host for feedback service
35
+ # production: feedback.push.apple.com
36
+ # development: feedback.sandbox.apple.com
37
+ @feedback_host = 'feedback.sandbox.push.apple.com'
38
+ @feedback_port = 2196
39
+
40
+ # openssl pkcs12 -in mycert.p12 -out client-cert.pem -nodes -clcerts
41
+ @pem = nil # this should be the path of the pem file not the contentes
42
+ @pass = nil
43
+
44
+ @cache_connections = false
45
+ @connections = {}
46
+
47
+ @truncate_mode = Truncate::TRUNCATE_METHOD_SOFT
48
+ @truncate_max_chopped = 10
49
+ @clean_whitespace = true
50
+
51
+ @logging = true
52
+
53
+ class << self
54
+ attr_accessor :host, :port, :feedback_host, :feedback_port, :pem, :pass, :cache_connections, :clean_whitespace, :truncate_mode, :truncate_max_chopped, :logging
55
+ end
56
+
57
+ def self.establish_notification_connection
58
+ if @cache_connections
59
+ begin
60
+ self.get_connection(self.host, self.port)
61
+ return true
62
+ rescue
63
+ end
64
+ end
65
+ return false
66
+ end
67
+
68
+ def self.has_notification_connection?
69
+ return self.has_connection?(self.host, self.port)
70
+ end
71
+
72
+ def self.send_notification(device_token, message, notification_id = rand(9999), expiry = (Time.now + 1.year))
73
+ self.with_notification_connection do |conn|
74
+ conn.write(self.packaged_notification(device_token, message, notification_id, expiry))
75
+ conn.flush
76
+ end
77
+ end
78
+
79
+ def self.send_notifications(notifications)
80
+ self.with_notification_connection do |conn|
81
+ notifications.each do |n|
82
+ conn.write(self.packaged_notification(n[0], n[1], (n[2] or rand(9999)), (n[3] or (Time.now + 1.year))))
83
+ end
84
+ conn.flush
85
+ end
86
+ end
87
+
88
+ def self.feedback
89
+ apns_feedback = []
90
+ self.with_feedback_connection do |conn|
91
+ # Read buffers data from the OS, so it's probably not
92
+ # too inefficient to do the small reads
93
+ while data = conn.read(38)
94
+ apns_feedback << self.parse_feedback_tuple(data)
95
+ end
96
+ end
97
+
98
+ return apns_feedback
99
+ end
100
+
101
+ protected
102
+
103
+ # Each tuple is in the following format:
104
+ #
105
+ # timestamp | token_length (32) | token
106
+ # bytes: 4 (big-endian) 2 (big-endian) | 32
107
+ #
108
+ # timestamp - seconds since the epoch, in UTC
109
+ # token_length - Always 32 for now
110
+ # token - 32 bytes of binary data specifying the device token
111
+ #
112
+ def self.parse_feedback_tuple(data)
113
+ feedback = data.unpack('N1n1H64')
114
+ {:feedback_at => Time.at(feedback[0]), :length => feedback[1], :device_token => feedback[2] }
115
+ end
116
+
117
+ def self.packaged_notification(device_token, message, identifier, expiry)
118
+ pt = self.packaged_token(device_token)
119
+ pm = self.packaged_message(message)
120
+ raise APNSException, "payload exceeds 256 byte limit" if pm.bytesize > 256
121
+
122
+ #return [0, 32, pt, pm.bytesize, pm].pack("cna*na*") # old format (NB. s> notation only compatible with ruby 1.9.3 and above)
123
+
124
+ expiry_unix = expiry.to_i
125
+ expiry_unix = 0 if expiry_unix < 0 # for APNS a zero timestamp has the same effect as a negative one and we are only encoding signed ints
126
+ puts "[APNS] packaging notification for device:[#{device_token}] identifier:#{identifier} expiry:#{expiry_unix} payload-size:(#{pm.bytesize}) payload:#{pm}" if @logging
127
+ [1, identifier.to_i, expiry_unix, 32, pt, pm.bytesize, pm].pack("cNNna*na*")
128
+ end
129
+
130
+ def self.packaged_token(device_token)
131
+ [device_token.gsub(/[\s|<|>]/,'')].pack('H*')
132
+ end
133
+
134
+ def self.packaged_message(message)
135
+ if message.is_a?(Hash)
136
+ hash = message
137
+ elsif message.is_a?(String)
138
+ hash = {:aps => {:alert => message } }
139
+ else
140
+ raise "Message needs to be either a hash or string"
141
+ end
142
+ hash = Truncate.truncate_notification(hash, @clean_whitespace, @truncate_mode, @truncate_max_chopped)
143
+ ApnsJSON.apns_json(hash)
144
+ end
145
+
146
+ def self.with_notification_connection(&block)
147
+ self.with_connection(self.host, self.port, &block)
148
+ end
149
+
150
+ def self.with_feedback_connection(&block)
151
+ # Explicitly disable the connection cache for feedback
152
+ cache_temp = @cache_connections
153
+ @cache_connections = false
154
+
155
+ self.with_connection(self.feedback_host, self.feedback_port, &block)
156
+
157
+ ensure
158
+ @cache_connections = cache_temp
159
+ end
160
+
161
+ private
162
+
163
+ def self.open_connection(host, port)
164
+ raise "The path to your pem file is not set. (APNS.pem = /path/to/cert.pem)" unless self.pem
165
+ raise "The path to your pem file does not exist!" unless File.exist?(self.pem)
166
+
167
+ context = OpenSSL::SSL::SSLContext.new
168
+ context.cert = OpenSSL::X509::Certificate.new(File.read(self.pem))
169
+ context.key = OpenSSL::PKey::RSA.new(File.read(self.pem), self.pass)
170
+
171
+ retries = 0
172
+ begin
173
+ sock = TCPSocket.new(host, port)
174
+ ssl = OpenSSL::SSL::SSLSocket.new(sock, context)
175
+ ssl.connect
176
+ return ssl, sock
177
+ rescue SystemCallError
178
+ if (retries += 1) < 5
179
+ sleep 1
180
+ retry
181
+ else
182
+ # Too many retries, re-raise this exception
183
+ raise
184
+ end
185
+ end
186
+ end
187
+
188
+ def self.has_connection?(host, port)
189
+ @connections.has_key?([host,port])
190
+ end
191
+
192
+ def self.create_connection(host, port)
193
+ @connections[[host, port]] = self.open_connection(host, port)
194
+ end
195
+
196
+ def self.find_connection(host, port)
197
+ @connections[[host, port]]
198
+ end
199
+
200
+ def self.remove_connection(host, port)
201
+ if self.has_connection?(host, port)
202
+ ssl, sock = @connections.delete([host, port])
203
+ ssl.close
204
+ sock.close
205
+ end
206
+ end
207
+
208
+ def self.reconnect_connection(host, port)
209
+ self.remove_connection(host, port)
210
+ self.create_connection(host, port)
211
+ end
212
+
213
+ def self.get_connection(host, port)
214
+ if @cache_connections
215
+ # Create a new connection if we don't have one
216
+ unless self.has_connection?(host, port)
217
+ self.create_connection(host, port)
218
+ end
219
+
220
+ ssl, sock = self.find_connection(host, port)
221
+ # If we're closed, reconnect
222
+ if ssl.closed?
223
+ self.reconnect_connection(host, port)
224
+ self.find_connection(host, port)
225
+ else
226
+ return [ssl, sock]
227
+ end
228
+ else
229
+ self.open_connection(host, port)
230
+ end
231
+ end
232
+
233
+ def self.with_connection(host, port, &block)
234
+ retries = 0
235
+ begin
236
+ ssl, sock = self.get_connection(host, port)
237
+ yield ssl if block_given?
238
+
239
+ unless @cache_connections
240
+ ssl.close
241
+ sock.close
242
+ end
243
+ rescue Errno::ECONNABORTED, Errno::EPIPE, Errno::ECONNRESET
244
+ if (retries += 1) < 5
245
+ self.remove_connection(host, port)
246
+ retry
247
+ else
248
+ # too-many retries, re-raise
249
+ raise
250
+ end
251
+ end
252
+ end
253
+
254
+ end
@@ -0,0 +1,32 @@
1
+ # Copyright (c) 2013 William Denniss
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person
4
+ # obtaining a copy of this software and associated documentation
5
+ # files (the "Software"), to deal in the Software without
6
+ # restriction, including without limitation the rights to use,
7
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the
9
+ # Software is furnished to do so, subject to the following
10
+ # conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ # OTHER DEALINGS IN THE SOFTWARE.
23
+ module APNS
24
+
25
+ class APNSException < Exception
26
+ end
27
+
28
+ class TrucateException < APNSException
29
+ end
30
+
31
+
32
+ end
@@ -0,0 +1,106 @@
1
+ # Copyright (c) 2013 William Denniss
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person
4
+ # obtaining a copy of this software and associated documentation
5
+ # files (the "Software"), to deal in the Software without
6
+ # restriction, including without limitation the rights to use,
7
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the
9
+ # Software is furnished to do so, subject to the following
10
+ # conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ # OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ module APNS
25
+
26
+ class Truncate
27
+
28
+ TRUNCATE_METHOD_SOFT = 'soft'
29
+ TRUNCATE_METHOD_HARD = 'hard'
30
+
31
+ NOTIFICATION_MAX_BYTE_SIZE = 256
32
+
33
+ # forces a notification to fit within Apple's payload limits by truncating the message as required
34
+ def self.truncate_notification(notification, clean_whitespace = true, truncate_mode = TRUNCATE_METHOD_SOFT, truncate_soft_max_chopped = 10)
35
+
36
+ raise ArgumentError, "notification is not a hash" unless notification.is_a?(Hash)
37
+ raise ArgumentError, "notification hash should contain :aps key" unless notification[:aps]
38
+
39
+ return notification if !notification[:aps][:alert] || notification[:aps][:alert] == ""
40
+
41
+ # cleans up whitespace
42
+ notification[:aps][:alert].gsub!(/([\s])+/, " ") if clean_whitespace
43
+
44
+ # wd: trims the notification payload to fit in 255 bytes
45
+ if ApnsJSON.apns_json_size(notification) > NOTIFICATION_MAX_BYTE_SIZE
46
+
47
+ oversize_by = ApnsJSON.apns_json_size(notification) - NOTIFICATION_MAX_BYTE_SIZE
48
+ message_target_byte_size = ApnsJSON.apns_json_size(notification[:aps][:alert]) - oversize_by
49
+
50
+ if message_target_byte_size < 0
51
+ raise TrucateException, "notification does not fit within 256 byte limit even by if the message was completely truncated"
52
+ end
53
+ if message_target_byte_size == 0
54
+ raise TrucateException, "notification would only fit within 256 byte limit by completely truncating the message which changes the presentation in iOS"
55
+ end
56
+
57
+ notification[:aps][:alert] = truncate_string(notification[:aps][:alert], message_target_byte_size, truncate_mode, truncate_soft_max_chopped)
58
+ end
59
+
60
+ return notification
61
+ end
62
+
63
+ # truncates a string to a given byte size
64
+ def self.truncate_string(input_string, target_byte_size, truncate_mode = TRUNCATE_METHOD_SOFT, truncate_soft_max_chopped = 10, ellipsis = "\u2026", truncate_soft_regex = /\s/)
65
+
66
+ raise ArgumentError, "Cannot truncate string to a negative number" if target_byte_size < 0
67
+
68
+ return input_string if ApnsJSON.apns_json_size(input_string) <= target_byte_size
69
+
70
+ # if the target size is below the size of the ellipsis, reverts to a hard-truncate with no ellipsis
71
+ if target_byte_size < ApnsJSON.apns_json_size(ellipsis)
72
+ truncate_mode = TRUNCATE_METHOD_HARD
73
+ ellipsis = ''
74
+ end
75
+
76
+ # reduces target-size by the ellipsis size
77
+ target_byte_size -= ApnsJSON.apns_json_size(ellipsis)
78
+
79
+ # starts with a string length equal to the target number bytes (which for an ASCII-only string is the final string)
80
+ string = input_string[0,target_byte_size]
81
+
82
+ # chops off characters one at a time until the byte-size is within our target size, to handle variable-byte char strings
83
+ while ApnsJSON.apns_json_size(string) > target_byte_size
84
+ string = string[0,string.length-1]
85
+ end
86
+
87
+ # further truncates string on whitespace boundaries
88
+ if truncate_mode == TRUNCATE_METHOD_SOFT
89
+
90
+ string = input_string[0, string.length+1] # elongates string by 1 character in case it happens to be whitespace
91
+ trim_to_index = string.rindex(/\s/) # sets trim index to be last whitespace character
92
+ if !trim_to_index || (string.length - trim_to_index) > truncate_soft_max_chopped
93
+ trim_to_index = string.length-1 # cancels soft-truncate if no whitespace, or too much would be chopped
94
+ end
95
+
96
+ string = string[0,trim_to_index]
97
+ end
98
+
99
+ string = '' if string.nil? # if string is nil, it was entirely truncated
100
+
101
+ return string + ellipsis
102
+ end
103
+
104
+ end
105
+
106
+ end
@@ -0,0 +1,3 @@
1
+ module APNS
2
+ VERSION = "0.2.0"
3
+ end
@@ -0,0 +1,84 @@
1
+ # Copyright (c) 2013 William Denniss
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person
4
+ # obtaining a copy of this software and associated documentation
5
+ # files (the "Software"), to deal in the Software without
6
+ # restriction, including without limitation the rights to use,
7
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the
9
+ # Software is furnished to do so, subject to the following
10
+ # conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ # OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ require 'spec_helper'
25
+
26
+ describe APNS do
27
+
28
+ it "should encode unicode to ascii-only json" do
29
+ string = "\u2601"
30
+ json = ApnsJSON.apns_json([string])
31
+ json.should == "[\"\u2601\"]"
32
+ end
33
+
34
+ it "should encode 5-byte unicode to JSON in an apple safe manner" do
35
+ string = "\u{1F511}"
36
+ json = ApnsJSON.apns_json([string])
37
+ json.should == "[\"\u{1F511}\"]"
38
+ end
39
+
40
+ it "should return JSON escaped" do
41
+ n = {:aps => {:alert => "\u2601Hello iPhone", :badge => 3, :sound => 'awesome.caf'}}
42
+ json = ApnsJSON.apns_json(n)
43
+ json.should == "{\"aps\":{\"alert\":\"\u2601Hello iPhone\",\"badge\":3,\"sound\":\"awesome.caf\"}}"
44
+ end
45
+
46
+ it "should throw exception if payload is too big" do
47
+ # too big with alert
48
+ notification = {:aps => {:alert => "#{LONG_MESSAGE_QBF}#{LONG_MESSAGE_QBF}", :badge => '1', :sound => 'default'}, :server_info => {:type => 'example', :data => 12345, :something => LONG_MESSAGE_QBF, :again => LONG_MESSAGE_QBF}}
49
+ expect { APNS.packaged_notification("12345678901234567890123456789012", notification, 1, Time.now + 14400) }.to raise_error(APNSException)
50
+
51
+ # too big without alert
52
+ notification = notification = {:aps => {:badge => '1', :sound => 'default'}, :server_info => {:type => 'example', :data => 12345, :something => LONG_MESSAGE_QBF, :again => LONG_MESSAGE_QBF}}
53
+ expect { APNS.packaged_notification("12345678901234567890123456789012", notification, 1, Time.now + 14400) }.to raise_error(APNSException)
54
+ end
55
+
56
+ describe '#packaged_message' do
57
+
58
+ it "should return JSON with notification information" do
59
+ n = APNS.packaged_message({:aps => {:alert => 'Hello iPhone', :badge => 3, :sound => 'awesome.caf'}})
60
+ n.should == "{\"aps\":{\"alert\":\"Hello iPhone\",\"badge\":3,\"sound\":\"awesome.caf\"}}"
61
+ end
62
+
63
+ it "should not include keys that are empty in the JSON" do
64
+ n = APNS.packaged_message({:aps => {:badge => 3}})
65
+ n.should == "{\"aps\":{\"badge\":3}}"
66
+ end
67
+
68
+ end
69
+
70
+ describe '#packaged_token' do
71
+ it "should package the token" do
72
+ n = APNS.packaged_token('<5b51030d d5bad758 fbad5004 bad35c31 e4e0f550 f77f20d4 f737bf8d 3d5524c6>')
73
+ Base64.encode64(n).should == "W1EDDdW611j7rVAEutNcMeTg9VD3fyDU9ze/jT1VJMY=\n"
74
+ end
75
+ end
76
+
77
+ describe '#packaged_notification' do
78
+ it "should package the notification" do
79
+ n = APNS.packaged_notification('device_token', {:aps => {:alert => 'Hello iPhone', :badge => 3, :sound => 'awesome.caf'}}, 1, 1367064263)
80
+ Base64.encode64(n).should == "AQAAAAFRe77HACDe8s79hOcAQHsiYXBzIjp7ImFsZXJ0IjoiSGVsbG8gaVBo\nb25lIiwiYmFkZ2UiOjMsInNvdW5kIjoiYXdlc29tZS5jYWYifX0=\n"
81
+ end
82
+ end
83
+
84
+ end
@@ -0,0 +1,180 @@
1
+ # encoding: UTF-8
2
+
3
+ # Copyright (c) 2013 William Denniss
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person
6
+ # obtaining a copy of this software and associated documentation
7
+ # files (the "Software"), to deal in the Software without
8
+ # restriction, including without limitation the rights to use,
9
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the
11
+ # Software is furnished to do so, subject to the following
12
+ # conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24
+ # OTHER DEALINGS IN THE SOFTWARE.
25
+
26
+ require 'spec_helper'
27
+
28
+ include APNS
29
+
30
+ describe APNS::Truncate do
31
+
32
+ STRING_1TO9 = "123456789"
33
+ STRING_QBF = "the quick brown fox jumps over the lazy dog"
34
+ STRING_CHINESE = "中国上海北京南京西安东京水茶可乐咖啡"
35
+ ELLIPSIS = "\u2026"
36
+ LONG_MESSAGE_QBF = "#{STRING_QBF}#{STRING_QBF}#{STRING_QBF}#{STRING_QBF}#{STRING_QBF}#{STRING_QBF}#{STRING_QBF}#{STRING_QBF}#{STRING_QBF}"
37
+ LONG_MESSAGE_CHINESE = "#{STRING_CHINESE}#{STRING_CHINESE}#{STRING_CHINESE}#{STRING_CHINESE}"
38
+
39
+ describe '#json_byte_length' do
40
+ it "should return the correct ascii-json byte size" do
41
+ # string sizes
42
+ ApnsJSON.apns_json_size("1").should == 1
43
+ ApnsJSON.apns_json_size("12").should == 2
44
+ ApnsJSON.apns_json_size("\u2026").should == 3
45
+ ApnsJSON.apns_json_size("\u{1F511}").should == 4
46
+
47
+ # object sizes
48
+ ApnsJSON.apns_json_size(["1"]).should == 1+4
49
+ ApnsJSON.apns_json_size(["12"]).should == 2+4
50
+ ApnsJSON.apns_json_size(["\u2026"]).should == 3+4
51
+ ApnsJSON.apns_json_size(["\u{1F511}"]).should == 4+4
52
+ end
53
+ end
54
+
55
+ describe '#truncate_string' do
56
+
57
+ it "should handle byte-sizes smaller than the ellipsis size" do
58
+ s = Truncate.truncate_string(STRING_1TO9, 2)
59
+ s.should == "12"
60
+ end
61
+
62
+ it "should handle byte-sizes equal to the ellipsis size" do
63
+ s = Truncate.truncate_string(STRING_1TO9, ApnsJSON.apns_json_size(ELLIPSIS))
64
+ s.should == ELLIPSIS
65
+
66
+ s = Truncate.truncate_string(STRING_CHINESE, ApnsJSON.apns_json_size(ELLIPSIS))
67
+ s.should == ELLIPSIS
68
+ end
69
+
70
+ it "should handle byte-sizes euqal to the ellipsis size+1" do
71
+ s = Truncate.truncate_string(STRING_1TO9, ApnsJSON.apns_json_size(ELLIPSIS)+1)
72
+ s.should == "1#{ELLIPSIS}"
73
+
74
+ s = Truncate.truncate_string(STRING_CHINESE, ApnsJSON.apns_json_size(ELLIPSIS)+1)
75
+ s.should == ELLIPSIS
76
+ end
77
+
78
+ it "should soft truncate stirngs to equal or below requested size #2" do
79
+
80
+ s = Truncate.truncate_string(STRING_QBF, 8)
81
+ s.bytesize.should <= 8
82
+
83
+ s = Truncate.truncate_string(STRING_CHINESE, 8)
84
+ s.bytesize.should <= 8
85
+
86
+ s = Truncate.truncate_string(STRING_QBF, 35)
87
+ s.bytesize.should <= 35
88
+
89
+ s = Truncate.truncate_string(STRING_CHINESE, 35)
90
+ s.bytesize.should <= 35
91
+ end
92
+
93
+ it "should hard-truncate to the exact str length" do
94
+ s = Truncate.truncate_string(STRING_QBF, 8, Truncate::TRUNCATE_METHOD_HARD, 0, '.')
95
+ s.bytesize.should == 8
96
+ end
97
+
98
+ it "should soft-truncate on word boundaries" do
99
+ s = Truncate.truncate_string(STRING_QBF, 9, Truncate::TRUNCATE_METHOD_SOFT, 10, '.')
100
+ s.should == "the."
101
+ end
102
+
103
+ it "shouldn't soft-truncate any more than needed (even if the space lies on the boundary)" do
104
+ s = Truncate.truncate_string(STRING_QBF, 10, Truncate::TRUNCATE_METHOD_SOFT, 10, '.')
105
+ s.should == "the quick."
106
+ end
107
+
108
+ it "soft truncate should handle strings with no spaces by reverting to hard-truncate" do
109
+ s = Truncate.truncate_string(STRING_1TO9, 8, Truncate::TRUNCATE_METHOD_SOFT, 10, '.')
110
+ s.bytesize.should == 8
111
+ end
112
+
113
+ it "soft truncate should handle unicode strings" do
114
+ s = Truncate.truncate_string(STRING_CHINESE, 20)
115
+ s.should_not == nil
116
+ end
117
+
118
+ it "should handle scripts that don't use whitespace and not soft-truncate more than needed" do
119
+ # soft truncate relies on spaces. Some scripts have no spaces and shouldn't be aversely truncated
120
+ soft = Truncate.truncate_string(STRING_CHINESE, 40, Truncate::TRUNCATE_METHOD_SOFT)
121
+ hard = Truncate.truncate_string(STRING_CHINESE, 40, Truncate::TRUNCATE_METHOD_HARD)
122
+ soft.should == hard
123
+ end
124
+
125
+ it "shouldn't truncate stirngs it doesn't need to" do
126
+ s = APNS::Truncate.truncate_string("123456789", 20)
127
+ s.should == "123456789"
128
+
129
+ s = APNS::Truncate.truncate_string("123456789", 9)
130
+ s.should == "123456789"
131
+
132
+ s = APNS::Truncate.truncate_string(STRING_CHINESE, 200)
133
+ s.should == STRING_CHINESE
134
+ end
135
+
136
+ end
137
+
138
+ describe '#truncate_notification' do
139
+
140
+ it "should truncate notification to size" do
141
+ notification = {:aps => {:alert => LONG_MESSAGE_QBF, :badge => '1', :sound => 'default'}, :server_info => {:type => 'example', :data => 12345, :something => 'blar'}}
142
+ Truncate.truncate_notification(notification)
143
+ ApnsJSON.apns_json(notification).bytesize.should <= 256
144
+
145
+ notification = {:aps => {:alert => LONG_MESSAGE_CHINESE, :badge => '1', :sound => 'default'}, :server_info => {:type => 'example', :data => 12345, :something => 'blar'}}
146
+ Truncate.truncate_notification(notification)
147
+ ApnsJSON.apns_json(notification).bytesize.should <= 256
148
+
149
+ notification = {:aps => {:alert => LONG_MESSAGE_QBF, :badge => '1', :sound => 'default'}, :server_info => {:type => 'example', :data => 12345, :something => 'blar'}}
150
+ Truncate.truncate_notification(notification, true, truncate_mode = Truncate::TRUNCATE_METHOD_HARD)
151
+ ApnsJSON.apns_json(notification).bytesize.should == 256
152
+ end
153
+
154
+ it "should throw exception if the notification cannot be truncated" do
155
+
156
+ notification = {:aps => {:alert => LONG_MESSAGE_QBF, :badge => '1', :sound => 'default'}, :server_info => {:type => 'example', :data => 12345, :something => LONG_MESSAGE_QBF, :again => LONG_MESSAGE_QBF}}
157
+ expect { Truncate.truncate_notification(notification) }.to raise_error
158
+ end
159
+
160
+ it "should throw exception if the notification structure is invalid" do
161
+
162
+ notification = {:alert => LONG_MESSAGE_QBF, :badge => '1', :sound => 'default'}
163
+ expect { Truncate.truncate_notification(notification) }.to raise_error (ArgumentError)
164
+
165
+ notification = [1,2,3]
166
+ expect { Truncate.truncate_notification(notification) }.to raise_error (ArgumentError)
167
+
168
+
169
+ end
170
+
171
+
172
+ it "shouldn't truncate notification unless needed" do
173
+ notification = {:aps => {:alert => STRING_QBF, :badge => '1', :sound => 'default'}, :server_info => {:type => 'example', :data => 12345, :something => 'blar'}}
174
+ Truncate.truncate_notification(notification)
175
+ notification[:aps][:alert].should == STRING_QBF
176
+ end
177
+
178
+ end
179
+
180
+ end
@@ -0,0 +1,2 @@
1
+ require 'apns'
2
+ require 'base64'
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gs-apns
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - William Denniss
9
+ - Paul Gebheim
10
+ - James Pozdena
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+ date: 2013-04-28 00:00:00.000000000 Z
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: rspec
18
+ requirement: !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ~>
22
+ - !ruby/object:Gem::Version
23
+ version: '2.6'
24
+ type: :development
25
+ prerelease: false
26
+ version_requirements: !ruby/object:Gem::Requirement
27
+ none: false
28
+ requirements:
29
+ - - ~>
30
+ - !ruby/object:Gem::Version
31
+ version: '2.6'
32
+ description: Simple Apple push notification service gem
33
+ email:
34
+ - will@geospike.com
35
+ executables: []
36
+ extensions: []
37
+ extra_rdoc_files:
38
+ - MIT-LICENSE
39
+ files:
40
+ - .gitignore
41
+ - Gemfile
42
+ - MIT-LICENSE
43
+ - README.textile
44
+ - Rakefile
45
+ - apns.gemspec
46
+ - lib/apns.rb
47
+ - lib/apns/apns_json.rb
48
+ - lib/apns/core.rb
49
+ - lib/apns/exceptions.rb
50
+ - lib/apns/truncate.rb
51
+ - lib/apns/version.rb
52
+ - spec/apns/core_spec.rb
53
+ - spec/apns/truncate_spec.rb
54
+ - spec/spec_helper.rb
55
+ homepage: http://github.com/WilliamDenniss/APNS
56
+ licenses: []
57
+ post_install_message:
58
+ rdoc_options: []
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ! '>='
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ! '>='
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements: []
74
+ rubyforge_project:
75
+ rubygems_version: 1.8.21
76
+ signing_key:
77
+ specification_version: 3
78
+ summary: Simple Apple push notification service gem
79
+ test_files:
80
+ - spec/apns/core_spec.rb
81
+ - spec/apns/truncate_spec.rb
82
+ - spec/spec_helper.rb