gs-apns 0.2.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.
- data/.gitignore +2 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +22 -0
- data/README.textile +158 -0
- data/Rakefile +2 -0
- data/apns.gemspec +20 -0
- data/lib/apns.rb +27 -0
- data/lib/apns/apns_json.rb +47 -0
- data/lib/apns/core.rb +254 -0
- data/lib/apns/exceptions.rb +32 -0
- data/lib/apns/truncate.rb +106 -0
- data/lib/apns/version.rb +3 -0
- data/spec/apns/core_spec.rb +84 -0
- data/spec/apns/truncate_spec.rb +180 -0
- data/spec/spec_helper.rb +2 -0
- metadata +82 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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
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
|
data/lib/apns/version.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
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
|