apns-pressplane 0.9.10
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/MIT-LICENSE +23 -0
- data/README.textile +199 -0
- data/Rakefile +53 -0
- data/apns.gemspec +31 -0
- data/lib/apns.rb +3 -0
- data/lib/apns/core.rb +162 -0
- data/lib/apns/device.rb +16 -0
- data/lib/apns/payload.rb +149 -0
- data/spec/apns/core_spec.rb +35 -0
- data/spec/apns/device_spec.rb +16 -0
- data/spec/apns/payload_spec.rb +137 -0
- data/spec/spec_helper.rb +5 -0
- metadata +64 -0
data/.gitignore
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
Copyright (c) 2009 James Pozdena
|
2
|
+
Copyright (c) 2010-2011 Thierry Passeron
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person
|
5
|
+
obtaining a copy of this software and associated documentation
|
6
|
+
files (the "Software"), to deal in the Software without
|
7
|
+
restriction, including without limitation the rights to use,
|
8
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the
|
10
|
+
Software is furnished to do so, subject to the following
|
11
|
+
conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
18
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
20
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
21
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
22
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
23
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.textile
ADDED
@@ -0,0 +1,199 @@
|
|
1
|
+
h1. APNS
|
2
|
+
|
3
|
+
A plugin/Gem for the Apple Push Notification Service.
|
4
|
+
|
5
|
+
The connections to Apple are done on demand (per process) and last until they are either closed by the system or are timed out.
|
6
|
+
This is the prefered way for communicating with Apple's push servers.
|
7
|
+
|
8
|
+
Works in Ruby on Rails 3.
|
9
|
+
|
10
|
+
h2. Install
|
11
|
+
|
12
|
+
<pre>
|
13
|
+
<code>
|
14
|
+
sudo gem install apns
|
15
|
+
or
|
16
|
+
rails plugin install git://...
|
17
|
+
</code>
|
18
|
+
</pre>
|
19
|
+
|
20
|
+
h2. Setup:
|
21
|
+
|
22
|
+
Convert your certificates
|
23
|
+
|
24
|
+
In Keychain access export your push certificate(s) as a .p12. Then run the following command to convert each .p12 to a .pem
|
25
|
+
|
26
|
+
<pre>
|
27
|
+
<code>
|
28
|
+
openssl pkcs12 -in cert.p12 -out cert.pem -nodes -clcerts
|
29
|
+
</code>
|
30
|
+
</pre>
|
31
|
+
|
32
|
+
After you have your .pem files, copy them to a place where APNS can access them.
|
33
|
+
You will need to register them with APNS before being able to send Notifications
|
34
|
+
|
35
|
+
In a Rails project, you can add an initializer to configure the pem(s), with for example:
|
36
|
+
|
37
|
+
<pre>
|
38
|
+
<code>
|
39
|
+
In file ./config/initializers/APNS.rb:
|
40
|
+
|
41
|
+
# Initialize the APNS environment
|
42
|
+
APNS.pem = Rails.root.join("config", Rails.env + ".pem") # => ./config/{development,production}.pem
|
43
|
+
|
44
|
+
</code>
|
45
|
+
</pre>
|
46
|
+
|
47
|
+
h2. Creating a Payload:
|
48
|
+
|
49
|
+
Sending a push notification is sending a Payload to Apple's servers.
|
50
|
+
|
51
|
+
You may create payloads with APNS::Payload.new(<device-token> [,<message>])
|
52
|
+
A payload is composed of a device-token and a message all mixed and encoded together.
|
53
|
+
Payload message can either just be a alert string or a hash that lets you specify the alert, badge, sound and any custom field.
|
54
|
+
|
55
|
+
<pre>
|
56
|
+
<code>
|
57
|
+
device_token = '123abc456def'
|
58
|
+
p1 = APNS::Payload.new(device_token, 'Hello iPhone!')
|
59
|
+
p2 = APNS::Payload.new(device_token, :alert => 'Hello iPhone!', :badge => 1, :sound => 'default')
|
60
|
+
p3 = APNS::Payload.new(device_token).badge(4).alert("Hello iPhone!").sound('bird.aiff')
|
61
|
+
|
62
|
+
# with custom data:
|
63
|
+
p4 = APNS::Payload.new(device_token, :badge => 2, :my_custom_field => 'blah')
|
64
|
+
p5 = APNS::Payload.new(device_token, :badge => 2).custom(:my_custom_field => 'blah')
|
65
|
+
</code>
|
66
|
+
</pre>
|
67
|
+
|
68
|
+
|
69
|
+
h2. Truncating payload informations
|
70
|
+
|
71
|
+
Only valid Payloads will be sent to Apple. From APNS point of view, a valid payload has a size lesser or equal to 256 bytes.
|
72
|
+
REM: Apple may find a APNS valid payload invalid because it doesn't contain mandatory fields in the message part.
|
73
|
+
For instance, a message must at least contain a non empty badge or sound or alert.
|
74
|
+
|
75
|
+
You can check whether a payload is valid with the Payload#valid? method.
|
76
|
+
In case you know a payload message field is too large and wish to have it truncated you can either use Payload#payload_with_truncated_alert or a more generic Payload#payload_with_truncated_string_at_keypath. These two method will try to truncate the value of the alert field or any custom field at a keypath.
|
77
|
+
|
78
|
+
Truncate the alert field:
|
79
|
+
|
80
|
+
<pre>
|
81
|
+
<code>
|
82
|
+
p = APNS::Payload.new("a-device-token", "A very long message "*15)
|
83
|
+
p.valid?
|
84
|
+
=> false
|
85
|
+
p.size
|
86
|
+
=> 331
|
87
|
+
p.payload_with_truncated_alert.size
|
88
|
+
=> 256
|
89
|
+
p.payload_with_truncated_alert.valid?
|
90
|
+
=> true
|
91
|
+
</code>
|
92
|
+
</pre>
|
93
|
+
|
94
|
+
Truncate a custom field:
|
95
|
+
|
96
|
+
<pre>
|
97
|
+
<code>
|
98
|
+
p = APNS::Payload.new("a-device-token", :alert => "Hello from APNS", :custom => {:foo => "Bar "*80})
|
99
|
+
p.valid?
|
100
|
+
=> false
|
101
|
+
p.size
|
102
|
+
=> 387
|
103
|
+
p.payload_with_truncated_string_at_keypath("custom.foo").size
|
104
|
+
=> 256
|
105
|
+
p.payload_with_truncated_string_at_keypath("custom.foo").valid?
|
106
|
+
=> true
|
107
|
+
</code>
|
108
|
+
</pre>
|
109
|
+
|
110
|
+
|
111
|
+
h2. Sending Notifications to a single application:
|
112
|
+
|
113
|
+
Before sending notifications, you _must_ have setup the pem file(s) so that Apple knows which application you are sending a notification to.
|
114
|
+
|
115
|
+
<pre>
|
116
|
+
<code>
|
117
|
+
APNS.pem = "/path/to/my/development.pem"
|
118
|
+
</code>
|
119
|
+
</pre>
|
120
|
+
|
121
|
+
Now we can send some payloads either with:
|
122
|
+
|
123
|
+
* APNS.send_payloads(<payloads>)
|
124
|
+
* APNS.send(<payloads>) # same as APNS.send_payloads
|
125
|
+
|
126
|
+
<pre>
|
127
|
+
<code>
|
128
|
+
APNS.send(p1, p2, p3)
|
129
|
+
</code>
|
130
|
+
</pre>
|
131
|
+
|
132
|
+
h2. Sending Notifications to multiple applications:
|
133
|
+
|
134
|
+
You may want to handle push notifications for many applications at once. In this case you have to setup multiple pem streams:
|
135
|
+
|
136
|
+
<pre>
|
137
|
+
<code>
|
138
|
+
@streams = [:voodoo, :child]
|
139
|
+
|
140
|
+
@streams.each do |stream|
|
141
|
+
APNS.pem(stream, "/path/to/#{stream}/development.pem"
|
142
|
+
end
|
143
|
+
</code>
|
144
|
+
</pre>
|
145
|
+
|
146
|
+
Now you can send the notifications to any stream with:
|
147
|
+
|
148
|
+
* APNS.send_stream(<stream>, <payloads>)
|
149
|
+
|
150
|
+
<pre>
|
151
|
+
<code>
|
152
|
+
APNS.send_stream(@streams.first, p1, p2, p3)
|
153
|
+
APNS.send_stream(@streams.last, p4, p5)
|
154
|
+
</code>
|
155
|
+
</pre>
|
156
|
+
|
157
|
+
|
158
|
+
h2. Feedback queue:
|
159
|
+
|
160
|
+
You should check the feedback queue of your application on Apple's servers to avoid sending notifications to obsolete devices
|
161
|
+
|
162
|
+
For single pem:
|
163
|
+
|
164
|
+
<pre>
|
165
|
+
<code>
|
166
|
+
APNS.feedback.each do |time, token|
|
167
|
+
# remove the device registered with this token ?
|
168
|
+
end
|
169
|
+
</code>
|
170
|
+
</pre>
|
171
|
+
|
172
|
+
For multiple pems:
|
173
|
+
|
174
|
+
<pre>
|
175
|
+
<code>
|
176
|
+
APNS.feedback(@streams.first).each do |time, token|
|
177
|
+
# remove the device registered with this token ?
|
178
|
+
end
|
179
|
+
</code>
|
180
|
+
</pre>
|
181
|
+
|
182
|
+
|
183
|
+
h2. Getting your iPhone's device token
|
184
|
+
|
185
|
+
After you setup push notification for your application with Apple. You need to ask Apple for you application specific device token.
|
186
|
+
|
187
|
+
In the UIApplicationDelegate
|
188
|
+
<pre>
|
189
|
+
<code>
|
190
|
+
- (void)applicationDidFinishLaunching:(UIApplication *)application {
|
191
|
+
[[UIApplication sharedApplication] registerForRemoteNotificationTypes:
|
192
|
+
(UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeSound | UIRemoteNotificationTypeBadge)];
|
193
|
+
}
|
194
|
+
|
195
|
+
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
|
196
|
+
// Do something with the device token
|
197
|
+
}
|
198
|
+
</code>
|
199
|
+
</pre>
|
data/Rakefile
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake/gempackagetask'
|
3
|
+
require 'rubygems/specification'
|
4
|
+
require 'date'
|
5
|
+
require 'rspec/core/rake_task'
|
6
|
+
|
7
|
+
GEM = 'apns'
|
8
|
+
GEM_NAME = 'apns'
|
9
|
+
GEM_VERSION = '0.9.9'
|
10
|
+
AUTHORS = ['James Pozdena', 'Thierry Passeron']
|
11
|
+
EMAIL = "thierry.passeron@gmail.com"
|
12
|
+
HOMEPAGE = "http://github.com/Orion98MC/APNS"
|
13
|
+
SUMMARY = "Simple Apple push notification service gem"
|
14
|
+
|
15
|
+
spec = Gem::Specification.new do |s|
|
16
|
+
s.name = GEM
|
17
|
+
s.version = GEM_VERSION
|
18
|
+
s.platform = Gem::Platform::RUBY
|
19
|
+
s.has_rdoc = true
|
20
|
+
s.extra_rdoc_files = ["MIT-LICENSE"]
|
21
|
+
s.summary = SUMMARY
|
22
|
+
s.description = s.summary
|
23
|
+
s.authors = AUTHORS
|
24
|
+
s.email = EMAIL
|
25
|
+
s.homepage = HOMEPAGE
|
26
|
+
s.require_path = 'lib'
|
27
|
+
s.autorequire = GEM
|
28
|
+
s.files = %w(MIT-LICENSE README.textile Rakefile) + Dir.glob("{lib}/**/*")
|
29
|
+
end
|
30
|
+
|
31
|
+
task :default => :spec
|
32
|
+
|
33
|
+
desc "Run specs"
|
34
|
+
RSpec::Core::RakeTask.new do |t|
|
35
|
+
t.pattern = 'spec/**/*_spec.rb'
|
36
|
+
t.rspec_opts = %w(-fs --color)
|
37
|
+
end
|
38
|
+
|
39
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
40
|
+
pkg.gem_spec = spec
|
41
|
+
end
|
42
|
+
|
43
|
+
desc "install the gem locally"
|
44
|
+
task :install => [:package] do
|
45
|
+
sh %{sudo gem install pkg/#{GEM}-#{GEM_VERSION}}
|
46
|
+
end
|
47
|
+
|
48
|
+
desc "create a gemspec file"
|
49
|
+
task :make_spec do
|
50
|
+
File.open("#{GEM}.gemspec", "w") do |file|
|
51
|
+
file.puts spec.to_ruby
|
52
|
+
end
|
53
|
+
end
|
data/apns.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{apns-pressplane}
|
5
|
+
s.version = "0.9.10"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["James Pozdena", "Thierry Passeron", "Pressplane Inc."]
|
9
|
+
s.autorequire = %q{apns}
|
10
|
+
s.date = %q{2010-03-22}
|
11
|
+
s.description = %q{Simple Apple push notification service gem}
|
12
|
+
s.email = %q{thierry.passeron@gmail.com}
|
13
|
+
s.extra_rdoc_files = ["MIT-LICENSE"]
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
|
+
s.homepage = %q{http://github.com/Orion98MC/APNS}
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
s.rubygems_version = %q{1.3.5}
|
20
|
+
s.summary = %q{Simple Apple push notification service gem}
|
21
|
+
|
22
|
+
if s.respond_to? :specification_version then
|
23
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
24
|
+
s.specification_version = 3
|
25
|
+
|
26
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
27
|
+
else
|
28
|
+
end
|
29
|
+
else
|
30
|
+
end
|
31
|
+
end
|
data/lib/apns.rb
ADDED
data/lib/apns/core.rb
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
module APNS
|
2
|
+
require 'socket'
|
3
|
+
require 'openssl'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
class PemPathError < RuntimeError;end
|
7
|
+
class PemFileError < RuntimeError;end
|
8
|
+
|
9
|
+
## Host for push notification service
|
10
|
+
#
|
11
|
+
# production: gateway.push.apple.com
|
12
|
+
# development: gateway.sandbox.apple.com
|
13
|
+
#
|
14
|
+
# You may set the correct host with:
|
15
|
+
# APNS.host = <host> or use the default one
|
16
|
+
@host = 'gateway.sandbox.push.apple.com'
|
17
|
+
@port = 2195
|
18
|
+
|
19
|
+
## Host for feedback service
|
20
|
+
#
|
21
|
+
# production: feedback.push.apple.com
|
22
|
+
# development: feedback.sandbox.apple.com
|
23
|
+
#
|
24
|
+
# You may set the correct feedback host with:
|
25
|
+
# APNS.feedback_host = <host> or use the default one
|
26
|
+
@feedback_host = @host.gsub('gateway','feedback')
|
27
|
+
@feedback_port = 2196
|
28
|
+
|
29
|
+
# openssl pkcs12 -in mycert.p12 -out client-cert.pem -nodes -clcerts
|
30
|
+
@pem = {} # this should be the path of the pem file not the contents
|
31
|
+
@pass = {}
|
32
|
+
|
33
|
+
# Persistent connection
|
34
|
+
@@ssl = {}
|
35
|
+
@@sock = {}
|
36
|
+
|
37
|
+
class << self
|
38
|
+
attr_accessor :host, :port, :feedback_host, :feedback_port
|
39
|
+
def pem(stream = :_global, new_pem = nil)
|
40
|
+
@pem[stream] = new_pem if new_pem
|
41
|
+
@pem[stream]
|
42
|
+
end
|
43
|
+
def pem=(new_pem); @pem[:_global] = new_pem; end
|
44
|
+
|
45
|
+
def pass(stream = :_global, new_pass = nil)
|
46
|
+
@pass[stream] = new_pass if new_pass
|
47
|
+
@pass[stream]
|
48
|
+
end
|
49
|
+
def pass=(new_pass); @pass[:_global] = new_pass; end
|
50
|
+
end
|
51
|
+
|
52
|
+
# send one or many payloads
|
53
|
+
#
|
54
|
+
# Connection
|
55
|
+
# The connection is made only if needed and is persisted until it times out or is closed by the system
|
56
|
+
#
|
57
|
+
# Errors
|
58
|
+
# If an error occures during the write operation, after 3 retries, the socket and ssl connections are closed and an exception is raised
|
59
|
+
#
|
60
|
+
# Example:
|
61
|
+
#
|
62
|
+
# single payload
|
63
|
+
# payload = APNS::Payload.new(device_token, 'Hello iPhone!')
|
64
|
+
# APNS.send_payloads(payload)
|
65
|
+
#
|
66
|
+
# or with multiple payloads
|
67
|
+
# APNS.send_payloads([payload1, payload2])
|
68
|
+
|
69
|
+
|
70
|
+
# Send to a pem stream
|
71
|
+
def self.send_stream(stream, *payloads)
|
72
|
+
payloads.flatten!
|
73
|
+
|
74
|
+
# retain valid payloads only
|
75
|
+
payloads.reject!{ |p| !(p.is_a?(APNS::Payload) && p.valid?) }
|
76
|
+
|
77
|
+
return if (payloads.nil? || payloads.count < 1)
|
78
|
+
|
79
|
+
# loop through each payloads
|
80
|
+
payloads.each do |payload|
|
81
|
+
retry_delay = 2
|
82
|
+
|
83
|
+
# !ToDo! do a better job by using a select to poll the socket for a possible response from apple to inform us about an error in the sent payload
|
84
|
+
#
|
85
|
+
begin
|
86
|
+
@@sock[stream], @@ssl[stream] = self.push_connection(stream) if @@ssl[stream].nil?
|
87
|
+
@@ssl[stream].write(payload.to_ssl); @@ssl[stream].flush
|
88
|
+
rescue PemPathError, PemFileError => e
|
89
|
+
raise e
|
90
|
+
rescue
|
91
|
+
@@ssl[stream].close; @@sock[stream].close
|
92
|
+
@@ssl[stream] = nil; @@sock[stream] = nil # cleanup
|
93
|
+
|
94
|
+
retry_delay *= 2
|
95
|
+
if retry_delay <= 8
|
96
|
+
sleep retry_delay
|
97
|
+
retry
|
98
|
+
else
|
99
|
+
raise
|
100
|
+
end
|
101
|
+
end # begin block
|
102
|
+
|
103
|
+
end # each payloads
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.send_payloads(*payloads)
|
107
|
+
self.send(payloads)
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.send(*payloads)
|
111
|
+
self.send_stream(:_global, payloads)
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
def self.feedback(stream = :_global)
|
116
|
+
sock, ssl = self.feedback_connection(stream)
|
117
|
+
|
118
|
+
apns_feedback = []
|
119
|
+
|
120
|
+
while line = sock.gets # Read lines from the socket
|
121
|
+
line.strip!
|
122
|
+
f = line.unpack('N1n1H140')
|
123
|
+
apns_feedback << [Time.at(f[0]), f[2]]
|
124
|
+
end
|
125
|
+
|
126
|
+
ssl.close
|
127
|
+
sock.close
|
128
|
+
|
129
|
+
return apns_feedback
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
protected
|
134
|
+
|
135
|
+
def self.ssl_context(stream = :_global)
|
136
|
+
raise PemPathError, "The path to your pem file is not set. (APNS.pem = /path/to/cert.pem)" unless self.pem(stream)
|
137
|
+
raise PemFileError, "The path to your pem file does not exist!" unless File.exist?(self.pem(stream))
|
138
|
+
|
139
|
+
context = OpenSSL::SSL::SSLContext.new
|
140
|
+
context.cert = OpenSSL::X509::Certificate.new(File.read(self.pem(stream)))
|
141
|
+
context.key = OpenSSL::PKey::RSA.new(File.read(self.pem(stream)), self.pass(stream))
|
142
|
+
context
|
143
|
+
end
|
144
|
+
|
145
|
+
def self.connect_to(aps_host, aps_port, stream = :_global)
|
146
|
+
context = self.ssl_context(stream)
|
147
|
+
sock = TCPSocket.new(aps_host, aps_port)
|
148
|
+
ssl = OpenSSL::SSL::SSLSocket.new(sock, context)
|
149
|
+
ssl.connect
|
150
|
+
|
151
|
+
return sock, ssl
|
152
|
+
end
|
153
|
+
|
154
|
+
def self.push_connection(stream = :_global)
|
155
|
+
self.connect_to(self.host, self.port, stream)
|
156
|
+
end
|
157
|
+
|
158
|
+
def self.feedback_connection(stream = :_global)
|
159
|
+
self.connect_to(self.feedback_host, self.feedback_port, stream)
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
data/lib/apns/device.rb
ADDED
data/lib/apns/payload.rb
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
module APNS
|
2
|
+
|
3
|
+
class Payload
|
4
|
+
attr_accessor :device, :message
|
5
|
+
|
6
|
+
APS_ROOT = :aps
|
7
|
+
APS_KEYS = [:alert, :badge, :sound]
|
8
|
+
|
9
|
+
PAYLOAD_MAX_SIZE = 256
|
10
|
+
|
11
|
+
def initialize(device_token, message_string_or_hash = {})
|
12
|
+
self.device = APNS::Device.new(device_token)
|
13
|
+
if message_string_or_hash.is_a?(String)
|
14
|
+
self.message = {:alert => message_string_or_hash.strip}
|
15
|
+
elsif message_string_or_hash.is_a?(Hash)
|
16
|
+
self.message = message_string_or_hash.each_value { |val| val.strip! if val.respond_to? :strip! }
|
17
|
+
else
|
18
|
+
raise "Payload message argument needs to be either a hash or a string"
|
19
|
+
end
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
# Batch payloads
|
25
|
+
# Ex: APNS::Payload.batch(Device.all.collect{|d|d.token}, :alert => "Hello")
|
26
|
+
# or with a block
|
27
|
+
# APNS::Payload.batch(Device.all.collect{|d|d.token}, :alert => custom_big_alert) do |payload|
|
28
|
+
# payload.payload_with_truncated_alert
|
29
|
+
# end
|
30
|
+
def self.batch(device_tokens, message_string_or_hash = {})
|
31
|
+
raise unless device_tokens.is_a?(Array)
|
32
|
+
payloads = []
|
33
|
+
device_tokens.each do |device|
|
34
|
+
payload = self.new(device, message_string_or_hash)
|
35
|
+
payload = yield(payload) if block_given?
|
36
|
+
payloads << payload
|
37
|
+
end
|
38
|
+
payloads
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# Handy chainable setters
|
43
|
+
#
|
44
|
+
# Ex: APNS::Payload.new(token).badge(3).sound("bipbip").alert("Roadrunner!").custom(:foo => :bar)
|
45
|
+
#
|
46
|
+
def badge(number)
|
47
|
+
message[:badge] = number
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
def sound(filename)
|
52
|
+
message[:sound] = filename
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
def alert(string)
|
57
|
+
message[:alert] = string
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
def custom(hash)
|
62
|
+
return nil unless hash.is_a? Hash
|
63
|
+
return nil if hash.any?{|k,v| APS_KEYS.include?(k.to_sym) || (k.to_sym == APS_ROOT)}
|
64
|
+
message.merge!(hash)
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
#
|
70
|
+
def to_ssl
|
71
|
+
pm = self.apn_message.to_json
|
72
|
+
[0, 0, 32, self.device.to_payload, 0, pm.size, pm].pack("ccca*cca*")
|
73
|
+
end
|
74
|
+
|
75
|
+
def size
|
76
|
+
self.to_ssl.size
|
77
|
+
end
|
78
|
+
|
79
|
+
# Validity checking only checks that the payload size is valid. We do not check the message content.
|
80
|
+
def valid?
|
81
|
+
self.size <= PAYLOAD_MAX_SIZE
|
82
|
+
end
|
83
|
+
|
84
|
+
def apn_message
|
85
|
+
message_hash = message.dup
|
86
|
+
apnm = { APS_ROOT => {} }
|
87
|
+
APS_KEYS.each do |k|
|
88
|
+
apnm[APS_ROOT][k] = message_hash.delete(k) if message_hash.has_key?(k)
|
89
|
+
end
|
90
|
+
apnm.merge!(message_hash)
|
91
|
+
apnm
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns a new payload with the alert truncated to fit in the payload size requirement (PAYLOAD_MAX_SIZE)
|
95
|
+
# Rem: It's a best effort since the alert may not be the one string responsible for the oversized payload
|
96
|
+
# also, if your alert is a Hash containing loc-* keys it won't work, in this case you should use the #payload_with_truncated_string_at_keypath
|
97
|
+
def payload_with_truncated_alert
|
98
|
+
payload_with_truncated_string_at_keypath([:alert])
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
# payload_with_truncated_string_at_keypath("alert") or payload_with_truncated_string_at_keypath([:alert])
|
103
|
+
# or
|
104
|
+
# payload_with_truncated_string_at_keypath("custom1.custom2") or payload_with_truncated_string_at_keypath([:custom1, :custom2])
|
105
|
+
# Rem: Truncation only works on String values...
|
106
|
+
def payload_with_truncated_string_at_keypath(array_or_dotted_string)
|
107
|
+
return self if valid? # You can safely call it on a valid payload
|
108
|
+
return unless message.at_key_path(array_or_dotted_string).is_a?(String)
|
109
|
+
|
110
|
+
# Rem: I'm using Marshall to make a deep copy of the message hash. Of course this would only work with "standard" values like Hash/String/Array
|
111
|
+
payload_with_empty_string = APNS::Payload.new(device.token, Marshal.load(Marshal.dump(message)).at_key_path(array_or_dotted_string){|obj, key| obj[key] = ""})
|
112
|
+
wanted_length = PAYLOAD_MAX_SIZE - payload_with_empty_string.size
|
113
|
+
|
114
|
+
# Return a new payload with truncated value
|
115
|
+
APNS::Payload.new(device.token, Marshal.load(Marshal.dump(message)).at_key_path(array_or_dotted_string) {|obj, key| obj[key] = obj[key].truncate(wanted_length) })
|
116
|
+
end
|
117
|
+
|
118
|
+
end #Payload
|
119
|
+
|
120
|
+
end #module
|
121
|
+
|
122
|
+
class Hash
|
123
|
+
def at_key_path(array_or_dotted_string, &block)
|
124
|
+
keypath = array_or_dotted_string.is_a?(Array) ? array_or_dotted_string.dup : array_or_dotted_string.split('.')
|
125
|
+
obj = self
|
126
|
+
while (keypath.count > 0) do
|
127
|
+
key = keypath.shift.to_s
|
128
|
+
key = key.to_sym if !obj.has_key?(key) && obj.has_key?(key.to_sym)
|
129
|
+
next unless keypath.count > 0 # exit the while loop
|
130
|
+
obj = obj.has_key?(key) ? obj[key] : raise("No key #{key} in Object (#{obj.inspect})")
|
131
|
+
end
|
132
|
+
|
133
|
+
raise("No key #{key} in Object (#{obj.inspect})") unless obj.has_key?(key)
|
134
|
+
if block_given?
|
135
|
+
block.call(obj, key)
|
136
|
+
return self
|
137
|
+
else
|
138
|
+
return obj[key]
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
class String
|
144
|
+
if !String.new.respond_to? :truncate
|
145
|
+
def truncate(len)
|
146
|
+
(len > 4 && length > 5) ? self[0..(len - 1) - 3] + '...' : self
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
describe APNS do
|
4
|
+
|
5
|
+
def valid_payload
|
6
|
+
APNS::Payload.new("a-device-token", "my message")
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "send_payload" do
|
10
|
+
it "should complain about no pem file path" do
|
11
|
+
lambda{APNS.send_payloads(valid_payload)}.should raise_error(APNS::PemPathError)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should complain about pem file inexistence" do
|
15
|
+
APNS.pem = "test.pem"
|
16
|
+
lambda{APNS.send_payloads(valid_payload)}.should raise_error(APNS::PemFileError)
|
17
|
+
APNS.pem = nil # cleanup for next tests
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "feedback" do
|
23
|
+
it "should complain about no pem file path" do
|
24
|
+
lambda{APNS.feedback()}.should raise_error(APNS::PemPathError)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should complain about pem file inexistence" do
|
28
|
+
APNS.pem = "test.pem"
|
29
|
+
lambda{APNS.feedback()}.should raise_error(APNS::PemFileError)
|
30
|
+
APNS.pem = nil # cleanup for next tests
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
describe APNS::Device do
|
4
|
+
before do
|
5
|
+
@device = APNS::Device.new('<5b51030d d5bad758 fbad5004 bad35c31 e4e0f550 f77f20d4 f737bf8d 3d5524c6>')
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should cleanup the token string" do
|
9
|
+
@device.token.should == "5b51030dd5bad758fbad5004bad35c31e4e0f550f77f20d4f737bf8d3d5524c6"
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should package the token for the payload" do
|
13
|
+
@device.to_payload.should == "[Q\003\r\325\272\327X\373\255P\004\272\323\\1\344\340\365P\367\177 \324\3677\277\215=U$\306"
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
describe APNS::Payload do
|
4
|
+
|
5
|
+
def payload(message, token = "a-device-token")
|
6
|
+
APNS::Payload.new(token, message)
|
7
|
+
end
|
8
|
+
it "should take a string as the message" do
|
9
|
+
p = payload('Hello')
|
10
|
+
p.should be_valid
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should take a hash as the message" do
|
14
|
+
p = payload({:alert => 'Hello iPhone', :badge => 3})
|
15
|
+
p.should be_valid
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should not validate a message longer than 256 bytes" do
|
19
|
+
p = payload({:alert => 'A' * 250})
|
20
|
+
p.size.should > 256
|
21
|
+
p.should_not be_valid
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should validate a message of 256 bytes or less" do
|
25
|
+
p = payload({:alert => 'A' * 224})
|
26
|
+
p.size.should == 256
|
27
|
+
p.should be_valid
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should strip whitespace from payload string" do
|
31
|
+
p = payload("Hello iPhone \n")
|
32
|
+
p.should be_valid
|
33
|
+
p.apn_message.should == { :aps => { :alert => 'Hello iPhone' } }
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should allow chaining of setters for badge alert and sound" do
|
37
|
+
p = APNS::Payload.new("a-device-token").badge(2).sound("bipbip.aiff").alert("hello")
|
38
|
+
p.message[:badge].should == 2
|
39
|
+
p.message[:sound].should == "bipbip.aiff"
|
40
|
+
p.message[:alert].should == "hello"
|
41
|
+
p.should be_valid
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should allow truncation of alert and return a valid payload when alert is too big" do
|
45
|
+
p = payload("A"*300)
|
46
|
+
p.size.should > 256
|
47
|
+
p.should_not be_valid
|
48
|
+
|
49
|
+
tp = p.payload_with_truncated_alert
|
50
|
+
tp.should be_valid
|
51
|
+
tp.size.should == 256
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should not change other fields but the alert message when truncating alert" do
|
55
|
+
p = payload({
|
56
|
+
:alert => "A" * 300,
|
57
|
+
:badge => 2,
|
58
|
+
:sound => "sound.aiff",
|
59
|
+
:custom => "my custom data"
|
60
|
+
})
|
61
|
+
p.should_not be_valid
|
62
|
+
p.size.should > 256
|
63
|
+
p.message[:badge].should == 2
|
64
|
+
p.message[:sound].should == "sound.aiff"
|
65
|
+
p.message[:custom].should == "my custom data"
|
66
|
+
|
67
|
+
pn = p.payload_with_truncated_alert
|
68
|
+
pn.should be_valid
|
69
|
+
pn.size.should == 256
|
70
|
+
pn.message[:badge].should == p.message[:badge]
|
71
|
+
pn.message[:sound].should == p.message[:sound]
|
72
|
+
pn.message[:custom].should == p.message[:custom]
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should not change the alert when truncating alert and alert is not a String" do
|
76
|
+
p = payload({
|
77
|
+
:alert => {'foo' => 'bar'*80, 'baz' => 'blah'},
|
78
|
+
:badge => 2,
|
79
|
+
:sound => "sound.aiff",
|
80
|
+
:custom => "my custom data"
|
81
|
+
})
|
82
|
+
p.should_not be_valid
|
83
|
+
|
84
|
+
pn = p.payload_with_truncated_alert
|
85
|
+
pn.should be_nil
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
it "should allow truncating a custom field" do
|
90
|
+
p = payload({
|
91
|
+
:alert => "my alert",
|
92
|
+
:badge => 2,
|
93
|
+
:sound => "sound.aiff",
|
94
|
+
:custom => {:message => "A"*300}
|
95
|
+
})
|
96
|
+
p.should_not be_valid
|
97
|
+
p.size.should > 256
|
98
|
+
p.message[:alert].should == "my alert"
|
99
|
+
p.message[:badge].should == 2
|
100
|
+
p.message[:sound].should == "sound.aiff"
|
101
|
+
|
102
|
+
pn = p.payload_with_truncated_string_at_keypath("custom.message")
|
103
|
+
pn.should be_valid
|
104
|
+
pn.size.should == 256
|
105
|
+
pn.message[:badge].should == p.message[:badge]
|
106
|
+
pn.message[:sound].should == p.message[:sound]
|
107
|
+
pn.message[:alert].should == p.message[:alert]
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
describe '#packaged_message' do
|
112
|
+
|
113
|
+
it "should return JSON with payload informations" do
|
114
|
+
p = payload({:alert => 'Hello iPhone', :badge => 2, :custom => "custom-string"})
|
115
|
+
p.apn_message.should == { :aps => { :badge => 2, :alert => 'Hello iPhone' }, :custom => "custom-string" }
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should return JSON with payload informations with whitespace stripped" do
|
119
|
+
p = payload({:alert => " Hello iPhone \n", :badge => 2, :custom => " custom-string \n"})
|
120
|
+
p.apn_message.should == { :aps => { :badge => 2, :alert => 'Hello iPhone' }, :custom => "custom-string" }
|
121
|
+
end
|
122
|
+
|
123
|
+
it "should not include keys that are empty in the JSON" do
|
124
|
+
p = payload({:badge => 3})
|
125
|
+
p.apn_message.should == { :aps => { :badge => 3 } }
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
describe '#to_ssl' do
|
131
|
+
it "should package the token and message" do
|
132
|
+
p = APNS::Payload.new('<5b51030d d5bad758 fbad5004 bad35c31 e4e0f550 f77f20d4 f737bf8d 3d5524c6>', {:alert => 'Hello iPhone'})
|
133
|
+
Base64.encode64(p.to_ssl).should == "AAAgW1EDDdW611j7rVAEutNcMeTg9VD3fyDU9ze/jT1VJMYAIHsiYXBzIjp7\nImFsZXJ0IjoiSGVsbG8gaVBob25lIn19\n"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: apns-pressplane
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.10
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- James Pozdena
|
9
|
+
- Thierry Passeron
|
10
|
+
- Pressplane Inc.
|
11
|
+
autorequire: apns
|
12
|
+
bindir: bin
|
13
|
+
cert_chain: []
|
14
|
+
date: 2010-03-22 00:00:00.000000000Z
|
15
|
+
dependencies: []
|
16
|
+
description: Simple Apple push notification service gem
|
17
|
+
email: thierry.passeron@gmail.com
|
18
|
+
executables: []
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files:
|
21
|
+
- MIT-LICENSE
|
22
|
+
files:
|
23
|
+
- .gitignore
|
24
|
+
- MIT-LICENSE
|
25
|
+
- README.textile
|
26
|
+
- Rakefile
|
27
|
+
- apns.gemspec
|
28
|
+
- lib/apns.rb
|
29
|
+
- lib/apns/core.rb
|
30
|
+
- lib/apns/device.rb
|
31
|
+
- lib/apns/payload.rb
|
32
|
+
- spec/apns/core_spec.rb
|
33
|
+
- spec/apns/device_spec.rb
|
34
|
+
- spec/apns/payload_spec.rb
|
35
|
+
- spec/spec_helper.rb
|
36
|
+
homepage: http://github.com/Orion98MC/APNS
|
37
|
+
licenses: []
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
requirements: []
|
55
|
+
rubyforge_project:
|
56
|
+
rubygems_version: 1.8.11
|
57
|
+
signing_key:
|
58
|
+
specification_version: 3
|
59
|
+
summary: Simple Apple push notification service gem
|
60
|
+
test_files:
|
61
|
+
- spec/apns/core_spec.rb
|
62
|
+
- spec/apns/device_spec.rb
|
63
|
+
- spec/apns/payload_spec.rb
|
64
|
+
- spec/spec_helper.rb
|