ghazel-em-apn 0.0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +85 -0
- data/Rakefile +9 -0
- data/certs/.gitignore +0 -0
- data/em-apn.gemspec +24 -0
- data/lib/em-apn/client.rb +78 -0
- data/lib/em-apn/connection.rb +47 -0
- data/lib/em-apn/error_response.rb +34 -0
- data/lib/em-apn/failed_delivery_attempt.rb +18 -0
- data/lib/em-apn/feedback_connection.rb +43 -0
- data/lib/em-apn/log_message.rb +13 -0
- data/lib/em-apn/notification.rb +62 -0
- data/lib/em-apn/response.rb +13 -0
- data/lib/em-apn/server.rb +58 -0
- data/lib/em-apn/test_helper.rb +37 -0
- data/lib/em-apn/version.rb +5 -0
- data/lib/em-apn.rb +26 -0
- data/lib/extensions/hash.rb +13 -0
- data/script/push +29 -0
- data/script/server +17 -0
- data/script/test +36 -0
- data/spec/em-apn/client_spec.rb +243 -0
- data/spec/em-apn/notification_spec.rb +182 -0
- data/spec/extensions/hash_spec.rb +38 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/support/certs/cert.pem +17 -0
- data/spec/support/certs/key.pem +15 -0
- metadata +121 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d304846e4e9cb3d8b7da79ef8a0e721dfb3a3ba0
|
4
|
+
data.tar.gz: bb079ada2ab1b5a9a1234a0ea698ba889c2bedfb
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cbdc74902f15c6f187fb0cb2a2ce9aba4954f4a20d42df0950636a45fc52a0b58c918a2d2a98dff32467f84c0aec41cba171f67617cb89fe02b8d5d9e116c238
|
7
|
+
data.tar.gz: 8670311ae9a6c723bdb82471c6b1651fcd89f439b9a2dbe1a1f915f0c3715be6295496748d8cdb6642b1e5044fa37faee2d19011fa3e74c49f44faa80dc18d5c
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
(The MIT-License)
|
2
|
+
|
3
|
+
Copyright (c) 2011 GroupMe
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following 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 OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# EM-APN - EventMachine'd Apple Push Notifications #
|
2
|
+
|
3
|
+
We want:
|
4
|
+
|
5
|
+
* Streamlined for a persistent connection use-case
|
6
|
+
* Support for the enhanced protocol, with receipts
|
7
|
+
|
8
|
+
## Usage ##
|
9
|
+
|
10
|
+
In a nutshell:
|
11
|
+
|
12
|
+
require "em-apn"
|
13
|
+
|
14
|
+
# Inside a reactor...
|
15
|
+
notification = EM::APN::Notification.new(token, :alert => alert)
|
16
|
+
client = EM::APN::Client.connect
|
17
|
+
client.deliver(notification)
|
18
|
+
|
19
|
+
Using this interface, the easiest way to configure the connection is by setting
|
20
|
+
some environment variables so that EM::APN can find your SSL certificates:
|
21
|
+
|
22
|
+
ENV["APN_KEY"] = "/path/to/key.pem"
|
23
|
+
ENV["APN_CERT"] = "/path/to/cert.pem"
|
24
|
+
|
25
|
+
Also, by default, the library connects to Apple's sandbox push server. If you
|
26
|
+
want to connect to the production server, simply set the `APN_ENV`
|
27
|
+
environment variable to `production`:
|
28
|
+
|
29
|
+
ENV["APN_ENV"] = "production"
|
30
|
+
|
31
|
+
The gateway and SSL certs can also be set directly when instantiating the object:
|
32
|
+
|
33
|
+
client = EM::APN::Client.connect(
|
34
|
+
:gateway => "some.host",
|
35
|
+
:key => "/path/to/key.pem",
|
36
|
+
:cert => "/path/to/cert.pem"
|
37
|
+
)
|
38
|
+
|
39
|
+
The client manages an underlying `EM::Connection`, and it will automatically
|
40
|
+
reconnect to the gateway when the connection is closed. Callbacks can be set
|
41
|
+
on the client to handle error responses from the gateway, connection open & close
|
42
|
+
events:
|
43
|
+
|
44
|
+
client = EM::APN::Client.connect
|
45
|
+
client.on_error do |response|
|
46
|
+
# See EM::APN::ErrorResponse
|
47
|
+
end
|
48
|
+
|
49
|
+
client.on_close do
|
50
|
+
# Do something.
|
51
|
+
end
|
52
|
+
|
53
|
+
client.on_open do
|
54
|
+
# Do something
|
55
|
+
end
|
56
|
+
|
57
|
+
In our experience, we've found that Apple immediately closes the connection
|
58
|
+
whenever an error is detected, so the error and close callbacks are nearly
|
59
|
+
always called one-to-one. These methods exist as a convenience, and the
|
60
|
+
callbacks can also be set directly to anything that responds to `#call`:
|
61
|
+
|
62
|
+
client.error_callback = Proc.new { |response| ... }
|
63
|
+
client.close_callback = Proc.new { ... }
|
64
|
+
client.open_callback = Proc.new { ... }
|
65
|
+
|
66
|
+
### Max Payload Size ###
|
67
|
+
|
68
|
+
Apple enforces a limit of __256 bytes__ for the __entire payload__.
|
69
|
+
|
70
|
+
If you attempt to deliver a notification that exceeds that limit, the library
|
71
|
+
will raise an `EM::APN::Notification::PayloadTooLarge` exception.
|
72
|
+
|
73
|
+
To prevent that from happening, you can call `#truncate_alert!` on the
|
74
|
+
notification.
|
75
|
+
|
76
|
+
notification = EM::APN::Notification.new(...)
|
77
|
+
notification.truncate_alert!
|
78
|
+
client.deliver(notification)
|
79
|
+
|
80
|
+
## Inspiration ##
|
81
|
+
|
82
|
+
Much thanks to:
|
83
|
+
|
84
|
+
* https://github.com/kdonovan/apn_sender
|
85
|
+
* http://blog.technopathllc.com/2010/12/apples-push-notification-with-ruby.html
|
data/Rakefile
ADDED
data/certs/.gitignore
ADDED
File without changes
|
data/em-apn.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- mode: ruby; encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "em-apn/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "ghazel-em-apn"
|
7
|
+
s.version = EventMachine::APN::VERSION
|
8
|
+
s.authors = ["Dave Yeu"]
|
9
|
+
s.email = ["daveyeu@gmail.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{EventMachine-driven Apple Push Notifications}
|
12
|
+
|
13
|
+
s.rubyforge_project = "em-apn"
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
s.add_dependency "eventmachine", ">= 1.0.0.beta.3"
|
21
|
+
s.add_dependency "multi_json", ">= 1.8.2"
|
22
|
+
|
23
|
+
s.add_development_dependency "rspec", "~> 2.6.0"
|
24
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module EventMachine
|
4
|
+
module APN
|
5
|
+
class Client
|
6
|
+
SANDBOX_GATEWAY = "gateway.sandbox.push.apple.com"
|
7
|
+
PRODUCTION_GATEWAY = "gateway.push.apple.com"
|
8
|
+
PORT = 2195
|
9
|
+
SANDBOX_FEEDBACK_GATEWAY = "feedback.sandbox.push.apple.com"
|
10
|
+
PRODUCTION_FEEDBACK_GATEWAY = "feedback.push.apple.com"
|
11
|
+
FEEDBACK_PORT = 2196
|
12
|
+
|
13
|
+
|
14
|
+
attr_reader :gateway, :port, :key, :cert, :connection, :error_callback, :close_callback, :open_callback
|
15
|
+
attr_reader :feedback_connection, :feedback_gateway, :feedback_port, :feedback_callback
|
16
|
+
|
17
|
+
# A convenience method for creating and connecting.
|
18
|
+
def self.connect(options = {})
|
19
|
+
new(options).tap do |client|
|
20
|
+
client.connect
|
21
|
+
client.connect_feedback
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(options = {})
|
26
|
+
@key = options[:key] || ENV["APN_KEY"]
|
27
|
+
@cert = options[:cert] || ENV["APN_CERT"]
|
28
|
+
@port = options[:port] || PORT
|
29
|
+
|
30
|
+
@gateway = options[:gateway] || ENV["APN_GATEWAY"]
|
31
|
+
@gateway ||= (ENV["APN_ENV"] == "production") ? PRODUCTION_GATEWAY : SANDBOX_GATEWAY
|
32
|
+
|
33
|
+
|
34
|
+
@feedback_gateway = options[:feedback_gateway] || ENV["APN_FEEDBACK_GATEWAY"]
|
35
|
+
@feedback_gateway ||= (ENV["APN_ENV"] == "production") ? PRODUCTION_FEEDBACK_GATEWAY : SANDBOX_FEEDBACK_GATEWAY
|
36
|
+
@feedback_port = options[:feedback_port] || FEEDBACK_PORT
|
37
|
+
|
38
|
+
@connection = nil
|
39
|
+
@feedback_connection = nil
|
40
|
+
end
|
41
|
+
|
42
|
+
def connect
|
43
|
+
@connection = EM.connect(gateway, port, Connection, self)
|
44
|
+
end
|
45
|
+
|
46
|
+
def connect_feedback
|
47
|
+
@feedback_connection = EM.connect(feedback_gateway, feedback_port, FeedbackConnection, self)
|
48
|
+
end
|
49
|
+
|
50
|
+
def deliver(notification)
|
51
|
+
notification.validate!
|
52
|
+
connect if connection.nil? || connection.disconnected?
|
53
|
+
log(notification)
|
54
|
+
connection.send_data(notification.data)
|
55
|
+
end
|
56
|
+
|
57
|
+
def on_error(&block)
|
58
|
+
@error_callback = block
|
59
|
+
end
|
60
|
+
|
61
|
+
def on_close(&block)
|
62
|
+
@close_callback = block
|
63
|
+
end
|
64
|
+
|
65
|
+
def on_open(&block)
|
66
|
+
@open_callback = block
|
67
|
+
end
|
68
|
+
|
69
|
+
def on_feedback(&block)
|
70
|
+
@feedback_callback = block
|
71
|
+
end
|
72
|
+
|
73
|
+
def log(notification)
|
74
|
+
EM::APN.logger.info("TOKEN=#{notification.token} PAYLOAD=#{notification.payload.inspect}")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module APN
|
3
|
+
class Connection < EM::Connection
|
4
|
+
attr_reader :client
|
5
|
+
|
6
|
+
def initialize(*args)
|
7
|
+
super
|
8
|
+
@client = args.last
|
9
|
+
@disconnected = false
|
10
|
+
end
|
11
|
+
|
12
|
+
def disconnected?
|
13
|
+
@disconnected
|
14
|
+
end
|
15
|
+
|
16
|
+
def post_init
|
17
|
+
start_tls(
|
18
|
+
:private_key_file => client.key,
|
19
|
+
:cert_chain_file => client.cert,
|
20
|
+
:verify_peer => false
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
def connection_completed
|
25
|
+
EM::APN.logger.info("Connection completed")
|
26
|
+
client.open_callback.call if client.open_callback
|
27
|
+
end
|
28
|
+
|
29
|
+
def receive_data(data)
|
30
|
+
data_array = data.unpack("ccN")
|
31
|
+
error_response = ErrorResponse.new(*data_array)
|
32
|
+
EM::APN.logger.warn(error_response.to_s)
|
33
|
+
|
34
|
+
if client.error_callback
|
35
|
+
client.error_callback.call(error_response)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def unbind
|
40
|
+
@disconnected = true
|
41
|
+
|
42
|
+
EM::APN.logger.info("Connection closed")
|
43
|
+
client.close_callback.call if client.close_callback
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module APN
|
3
|
+
class ErrorResponse
|
4
|
+
DESCRIPTION = {
|
5
|
+
0 => "No errors encountered",
|
6
|
+
1 => "Processing error",
|
7
|
+
2 => "Missing device token",
|
8
|
+
3 => "Missing topic",
|
9
|
+
4 => "Missing payload",
|
10
|
+
5 => "Invalid token size",
|
11
|
+
6 => "Invalid topic size",
|
12
|
+
7 => "Invalid payload size",
|
13
|
+
8 => "Invalid token",
|
14
|
+
255 => "None (unknown)"
|
15
|
+
}
|
16
|
+
|
17
|
+
attr_reader :command, :status_code, :identifier
|
18
|
+
|
19
|
+
def initialize(command, status_code, identifier)
|
20
|
+
@command = command
|
21
|
+
@status_code = status_code
|
22
|
+
@identifier = identifier
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
"CODE=#{@status_code} ID=#{@identifier} DESC=#{description}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def description
|
30
|
+
DESCRIPTION[@status_code] || "Missing description"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module APN
|
3
|
+
class FailedDeliveryAttempt
|
4
|
+
LENGTH = 38
|
5
|
+
|
6
|
+
attr_accessor :timestamp, :device_token
|
7
|
+
|
8
|
+
def initialize(binary_tuple)
|
9
|
+
# N => 4 byte timestamp
|
10
|
+
# n => 2 byte token_length
|
11
|
+
# H64 => 32 byte device_token
|
12
|
+
seconds, _, @device_token = binary_tuple.unpack('NnH64')
|
13
|
+
raise ArgumentError('invalid format') unless seconds && @device_token
|
14
|
+
@timestamp = Time.at(seconds)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module APN
|
3
|
+
class FeedbackConnection < EM::Connection
|
4
|
+
attr_reader :client
|
5
|
+
|
6
|
+
def initialize(*args)
|
7
|
+
super
|
8
|
+
@client = args.last
|
9
|
+
@disconnected = false
|
10
|
+
end
|
11
|
+
|
12
|
+
def disconnected?
|
13
|
+
@disconnected
|
14
|
+
end
|
15
|
+
|
16
|
+
def post_init
|
17
|
+
start_tls(
|
18
|
+
:private_key_file => client.key,
|
19
|
+
:cert_chain_file => client.cert,
|
20
|
+
:verify_peer => false
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
def connection_completed
|
25
|
+
EM::APN.logger.info("Feedback connection completed")
|
26
|
+
end
|
27
|
+
|
28
|
+
def receive_data(data)
|
29
|
+
attempt = FailedDeliveryAttempt.new(data)
|
30
|
+
EM::APN.logger.warn(attempt.to_s)
|
31
|
+
|
32
|
+
if client.feedback_callback
|
33
|
+
client.feedback_callback.call(attempt)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def unbind
|
38
|
+
@disconnected = true
|
39
|
+
EM::APN.logger.info("Feedback connection closed")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module EventMachine
|
4
|
+
module APN
|
5
|
+
class Notification
|
6
|
+
DATA_MAX_BYTES = 256
|
7
|
+
ALERT_KEY = "alert"
|
8
|
+
|
9
|
+
class PayloadTooLarge < StandardError; end
|
10
|
+
|
11
|
+
attr_reader :token, :identifier
|
12
|
+
attr_accessor :expiry
|
13
|
+
|
14
|
+
def initialize(token, aps = {}, custom = {}, options = {})
|
15
|
+
raise "Bad push token: #{token}" if token.nil? || (token.length != 64)
|
16
|
+
|
17
|
+
@token = token
|
18
|
+
@aps = aps.stringify_keys!
|
19
|
+
@custom = custom
|
20
|
+
@expiry = options[:expiry]
|
21
|
+
|
22
|
+
self.identifier = options[:identifier] if options[:identifier]
|
23
|
+
end
|
24
|
+
|
25
|
+
def payload
|
26
|
+
MultiJson.encode(@custom.merge(:aps => @aps))
|
27
|
+
end
|
28
|
+
|
29
|
+
# Documentation about this format is here:
|
30
|
+
# http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html
|
31
|
+
def data
|
32
|
+
identifier = @identifier || 0
|
33
|
+
expiry = @expiry || 0
|
34
|
+
size = [payload].pack("a*").size
|
35
|
+
data_array = [1, identifier, expiry, 32, token, size, payload]
|
36
|
+
data_array.pack("cNNnH*na*")
|
37
|
+
end
|
38
|
+
|
39
|
+
def validate!
|
40
|
+
if data.size > DATA_MAX_BYTES
|
41
|
+
error = "max is #{DATA_MAX_BYTES} bytes, but got #{data.size}: #{payload.inspect}"
|
42
|
+
raise PayloadTooLarge.new(error)
|
43
|
+
else
|
44
|
+
true
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def identifier=(new_identifier)
|
49
|
+
@identifier = new_identifier.to_i
|
50
|
+
end
|
51
|
+
|
52
|
+
def truncate_alert!
|
53
|
+
return unless @aps.has_key?(ALERT_KEY)
|
54
|
+
|
55
|
+
while data.size > DATA_MAX_BYTES && @aps[ALERT_KEY].size > 0
|
56
|
+
@aps[ALERT_KEY].chop!
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# Mock Apple push server... because we love to test
|
3
|
+
module EventMachine
|
4
|
+
module APN
|
5
|
+
module Server
|
6
|
+
def post_init
|
7
|
+
EM::APN.logger.info("Received a new connection")
|
8
|
+
@data = ""
|
9
|
+
|
10
|
+
start_tls(
|
11
|
+
:cert_chain_file => ENV["APN_CERT"],
|
12
|
+
:private_key_file => ENV["APN_KEY"],
|
13
|
+
:verify_peer => false
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
def ssl_handshake_completed
|
18
|
+
EM::APN.logger.info("SSL handshake completed")
|
19
|
+
end
|
20
|
+
|
21
|
+
def receive_data(data)
|
22
|
+
@data << data
|
23
|
+
|
24
|
+
# Try to extract the payload header
|
25
|
+
headers = @data.unpack("cNNnH64n")
|
26
|
+
return if headers.last.nil?
|
27
|
+
|
28
|
+
# Try to grab the payload
|
29
|
+
payload_size = headers.last
|
30
|
+
payload = @data[45, payload_size]
|
31
|
+
return if payload.length != payload_size
|
32
|
+
|
33
|
+
@data = @data[45 + payload_size, -1] || ""
|
34
|
+
|
35
|
+
process(headers, payload)
|
36
|
+
end
|
37
|
+
|
38
|
+
def process(headers, payload)
|
39
|
+
message = "APN RECV #{headers[4]} #{payload}"
|
40
|
+
EM::APN.logger.info(message)
|
41
|
+
|
42
|
+
args = MultiJson.decode(payload)
|
43
|
+
|
44
|
+
# If the alert is 'DISCONNECT', then we fake a bad payload by replying
|
45
|
+
# with an error and disconnecting.
|
46
|
+
if args["aps"]["alert"] == "DISCONNECT"
|
47
|
+
EM::APN.logger.info("Disconnecting")
|
48
|
+
send_data([8, 1, 0].pack("ccN"))
|
49
|
+
close_connection_after_writing
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def unbind
|
54
|
+
EM::APN.logger.info("Connection closed")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# Test helper for EM::APN
|
2
|
+
#
|
3
|
+
# To use this, start by simply requiring this file after EM::APN has already
|
4
|
+
# been loaded
|
5
|
+
#
|
6
|
+
# require "em-apn"
|
7
|
+
# require "em-apn/test_helper"
|
8
|
+
#
|
9
|
+
# This will nullify actual deliveries and instead, push them onto an accessible
|
10
|
+
# list:
|
11
|
+
#
|
12
|
+
# expect {
|
13
|
+
# client.deliver('notification)
|
14
|
+
# }.to change { EM::APN.deliveries.size }.by(1)
|
15
|
+
#
|
16
|
+
# notification = EM::APN.deliveries.first
|
17
|
+
# notification.should be_an_instance_of(EM::APN::Notification)
|
18
|
+
# notification.payload.should == ...
|
19
|
+
#
|
20
|
+
module EventMachine
|
21
|
+
module APN
|
22
|
+
def self.deliveries
|
23
|
+
@deliveries ||= []
|
24
|
+
end
|
25
|
+
|
26
|
+
Client.class_eval do
|
27
|
+
def connect
|
28
|
+
# No-op
|
29
|
+
end
|
30
|
+
|
31
|
+
def deliver(notification)
|
32
|
+
log(notification)
|
33
|
+
EM::APN.deliveries << notification
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/em-apn.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require "eventmachine"
|
4
|
+
require "multi_json"
|
5
|
+
require "logger"
|
6
|
+
require "extensions/hash"
|
7
|
+
require "em-apn/client"
|
8
|
+
require "em-apn/feedback_connection"
|
9
|
+
require 'em-apn/failed_delivery_attempt'
|
10
|
+
require "em-apn/connection"
|
11
|
+
require "em-apn/notification"
|
12
|
+
require "em-apn/log_message"
|
13
|
+
require "em-apn/response"
|
14
|
+
require "em-apn/error_response"
|
15
|
+
|
16
|
+
module EventMachine
|
17
|
+
module APN
|
18
|
+
def self.logger
|
19
|
+
@logger ||= Logger.new(STDOUT)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.logger=(new_logger)
|
23
|
+
@logger = new_logger
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/script/push
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Send a single push notification
|
4
|
+
#
|
5
|
+
# $ script/push <TOKEN> "<ALERT>"
|
6
|
+
#
|
7
|
+
|
8
|
+
require "rubygems"
|
9
|
+
require "bundler/setup"
|
10
|
+
require "em-apn"
|
11
|
+
|
12
|
+
ENV["APN_KEY"] ||= File.join(File.dirname(__FILE__), "..", "certs", "key.pem")
|
13
|
+
ENV["APN_CERT"] ||= File.join(File.dirname(__FILE__), "..", "certs", "cert.pem")
|
14
|
+
|
15
|
+
token, alert = ARGV
|
16
|
+
|
17
|
+
if token.nil? || alert.nil?
|
18
|
+
puts "Usage: script/push <TOKEN> \"<ALERT>\""
|
19
|
+
exit 1
|
20
|
+
end
|
21
|
+
|
22
|
+
EM.run do
|
23
|
+
client = EM::APN::Client.connect
|
24
|
+
client.deliver(EM::APN::Notification.new(token, :alert => alert))
|
25
|
+
|
26
|
+
# Hopefully give ourselves enough time to receive a response on failure.
|
27
|
+
# Wish there was a better way to do this. Or at least a more timely way.
|
28
|
+
EM.add_timer(1) { EM.stop_event_loop }
|
29
|
+
end
|
data/script/server
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Run a mock Apple push server
|
4
|
+
|
5
|
+
require "rubygems"
|
6
|
+
require "bundler/setup"
|
7
|
+
require "em-apn"
|
8
|
+
require "em-apn/server"
|
9
|
+
|
10
|
+
ENV["APN_KEY"] = File.join(File.dirname(__FILE__), "..", "certs", "key.pem")
|
11
|
+
ENV["APN_CERT"] = File.join(File.dirname(__FILE__), "..", "certs", "cert.pem")
|
12
|
+
|
13
|
+
EM::APN.logger.info("Starting push server")
|
14
|
+
|
15
|
+
EM.run do
|
16
|
+
EM.start_server("127.0.0.1", 2195, EM::APN::Server)
|
17
|
+
end
|