mameapns 0.0.1
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 +27 -0
- data/Gemfile +4 -0
- data/Guardfile +9 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +11 -0
- data/bin/mameapns +130 -0
- data/lib/mameapns.rb +19 -0
- data/lib/mameapns/application.rb +103 -0
- data/lib/mameapns/connection.rb +110 -0
- data/lib/mameapns/error.rb +20 -0
- data/lib/mameapns/interruptible_sleep.rb +16 -0
- data/lib/mameapns/notification.rb +47 -0
- data/lib/mameapns/options.rb +56 -0
- data/lib/mameapns/session.rb +115 -0
- data/lib/mameapns/session/deliver.rb +57 -0
- data/lib/mameapns/session/feedback.rb +24 -0
- data/lib/mameapns/version.rb +3 -0
- data/mameapns.gemspec +24 -0
- data/spec/mameapns/notification_spec.rb +65 -0
- data/spec/spec_helper.rb +6 -0
- metadata +149 -0
data/.gitignore
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
.rspec
|
7
|
+
Gemfile.lock
|
8
|
+
InstalledFiles
|
9
|
+
_yardoc
|
10
|
+
coverage
|
11
|
+
doc/
|
12
|
+
lib/bundler/man
|
13
|
+
pkg
|
14
|
+
rdoc
|
15
|
+
spec/reports
|
16
|
+
test/tmp
|
17
|
+
test/version_tmp
|
18
|
+
tmp
|
19
|
+
|
20
|
+
vendor
|
21
|
+
|
22
|
+
# for emacs
|
23
|
+
*~
|
24
|
+
\#*
|
25
|
+
.\#*
|
26
|
+
|
27
|
+
UTF-8
|
data/Gemfile
ADDED
data/Guardfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 sutetotanuki
|
2
|
+
|
3
|
+
MIT License
|
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,29 @@
|
|
1
|
+
# Mameapns
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'mameapns'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install mameapns
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/bin/mameapns
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler"
|
4
|
+
Bundler.setup
|
5
|
+
require "thor"
|
6
|
+
|
7
|
+
$:.unshift File.expand_path("../../lib", __FILE__)
|
8
|
+
|
9
|
+
require "mameapns"
|
10
|
+
|
11
|
+
module Mameapns
|
12
|
+
class MameDRb
|
13
|
+
def initialize(queue)
|
14
|
+
@queue = queue
|
15
|
+
end
|
16
|
+
|
17
|
+
def deliver(token, alert)
|
18
|
+
@queue.push([token, alert])
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
class Mameapns::Runner < Thor
|
25
|
+
desc "deliver", "send a notification to your device."
|
26
|
+
option :cert, type: :string, aliases: "-c", desc: "certificate file", required: true
|
27
|
+
option :pass, type: :string, aliases: "-p", desc: "certificate password", required: true
|
28
|
+
option :token, type: :string, aliases: "-t", desc: "target devise token", required: true
|
29
|
+
option :develop, type: :boolean, aliases: "-d", desc: "developer mode", default: false
|
30
|
+
|
31
|
+
def deliver
|
32
|
+
unless File.file?(options[:cert])
|
33
|
+
puts "#{options[:cert]} is invalid file or not exists."
|
34
|
+
exit(1)
|
35
|
+
end
|
36
|
+
|
37
|
+
cert = File.read(options[:cert])
|
38
|
+
pass = options[:pass]
|
39
|
+
|
40
|
+
@apns = Mameapns.new(
|
41
|
+
ssl_cert: cert,
|
42
|
+
ssl_cert_key: cert,
|
43
|
+
ssl_cert_pass: pass,
|
44
|
+
develop: options[:develop]
|
45
|
+
)
|
46
|
+
|
47
|
+
@apns.on_sent do |notification|
|
48
|
+
puts "Sent notification complete."
|
49
|
+
p notification
|
50
|
+
@apns.stop
|
51
|
+
end
|
52
|
+
|
53
|
+
@apns.on_delivery_error do |notification, error|
|
54
|
+
puts "error occured during sending notification."
|
55
|
+
p error
|
56
|
+
@apns.stop
|
57
|
+
end
|
58
|
+
|
59
|
+
@apns.on_exception do |error|
|
60
|
+
puts "critical error occured during sending notification."
|
61
|
+
p error
|
62
|
+
@apns.stop
|
63
|
+
end
|
64
|
+
|
65
|
+
@apns.start
|
66
|
+
|
67
|
+
@apns.deliver(
|
68
|
+
Mameapns::Notification.new(
|
69
|
+
device_token: options[:token],
|
70
|
+
alert: "test message"
|
71
|
+
))
|
72
|
+
|
73
|
+
@apns.wait_stop
|
74
|
+
end
|
75
|
+
|
76
|
+
desc "drb", "Run drb server that provides handle apns notifications."
|
77
|
+
option :cert, type: :string, aliases: "-c", desc: "certificate file", required: true
|
78
|
+
option :pass, type: :string, aliases: "-p", desc: "certificate password", required: true
|
79
|
+
option :develop, type: :boolean, aliases: "-d", desc: "developer mode", default: false
|
80
|
+
option :port, type: :numeric, aliases: "-P", desc: "listen port", default: 11875
|
81
|
+
def drb
|
82
|
+
require "drb/drb"
|
83
|
+
require "thread"
|
84
|
+
|
85
|
+
unless File.file?(options[:cert])
|
86
|
+
puts "#{options[:cert]} is invalid file or not exists."
|
87
|
+
exit(1)
|
88
|
+
end
|
89
|
+
|
90
|
+
@queue = Queue.new
|
91
|
+
|
92
|
+
cert = File.read(options[:cert])
|
93
|
+
pass = options[:pass]
|
94
|
+
|
95
|
+
@apns = Mameapns.new(
|
96
|
+
ssl_cert: cert,
|
97
|
+
ssl_cert_key: cert,
|
98
|
+
ssl_cert_pass: pass,
|
99
|
+
develop: options[:develop]
|
100
|
+
)
|
101
|
+
|
102
|
+
@apns.start
|
103
|
+
|
104
|
+
drb_server = DRb::DRbServer.new("druby://localhost:#{options[:port]}", Mameapns::MameDRb.new(@queue))
|
105
|
+
|
106
|
+
[:TERM, :QUIT, :INT].each do |sig|
|
107
|
+
trap(sig) do
|
108
|
+
puts "Going to shutdown..."
|
109
|
+
drb_server.stop_service
|
110
|
+
@apns.stop
|
111
|
+
@apns.wait_stop
|
112
|
+
exit(1)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
|
117
|
+
while tuple = @queue.pop
|
118
|
+
@apns.deliver(
|
119
|
+
Mameapns::Notification.new(
|
120
|
+
device_token: tuple[0],
|
121
|
+
alert: tuple[1]
|
122
|
+
))
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
# drb_server.stop_server
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
Mameapns::Runner.start
|
data/lib/mameapns.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require "thread"
|
2
|
+
require "openssl"
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module Mameapns
|
6
|
+
require "mameapns/error"
|
7
|
+
require "mameapns/version"
|
8
|
+
|
9
|
+
autoload :Options, 'mameapns/options'
|
10
|
+
autoload :Notification, 'mameapns/notification'
|
11
|
+
autoload :Application, 'mameapns/application'
|
12
|
+
autoload :Session, 'mameapns/session'
|
13
|
+
autoload :InterruptibleSleep, 'mameapns/interruptible_sleep'
|
14
|
+
autoload :Connection, 'mameapns/connection'
|
15
|
+
|
16
|
+
def self.new(options)
|
17
|
+
Application.new(options)
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Mameapns
|
2
|
+
class Application
|
3
|
+
include Options
|
4
|
+
|
5
|
+
options :host
|
6
|
+
options :port, default: 2195
|
7
|
+
options :feedback_host
|
8
|
+
options :feedback_port, default: 2196
|
9
|
+
options :ssl_cert
|
10
|
+
options :ssl_cert_key
|
11
|
+
options :ssl_cert_pass
|
12
|
+
options :develop, default: false
|
13
|
+
|
14
|
+
def host
|
15
|
+
@host ||= if develop
|
16
|
+
"gateway.sandbox.push.apple.com"
|
17
|
+
else
|
18
|
+
"gateway.push.apple.com"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def feedback_host
|
23
|
+
@feedback_host ||= if develop
|
24
|
+
"feedback.sandbox.push.apple.com"
|
25
|
+
else
|
26
|
+
"feedback.push.apple.com"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def start
|
31
|
+
@deliver_session = Session::Deliver.new(
|
32
|
+
ssl_cert: ssl_cert,
|
33
|
+
ssl_cert_key: ssl_cert_key,
|
34
|
+
ssl_cert_pass: ssl_cert_pass,
|
35
|
+
host: host,
|
36
|
+
port: port
|
37
|
+
)
|
38
|
+
|
39
|
+
@feedback_session = Session::Feedback.new(
|
40
|
+
ssl_cert: ssl_cert,
|
41
|
+
ssl_cert_key: ssl_cert_key,
|
42
|
+
ssl_cert_pass: ssl_cert_pass,
|
43
|
+
host: feedback_host,
|
44
|
+
port: feedback_port
|
45
|
+
)
|
46
|
+
|
47
|
+
@deliver_session.on_sent(&method(:handle_sent))
|
48
|
+
@deliver_session.on_error(&method(:handle_delivery_error))
|
49
|
+
@deliver_session.on_exception(&method(:handle_exception))
|
50
|
+
|
51
|
+
@feedback_session.on_error(&method(:handle_feedback))
|
52
|
+
@feedback_session.on_exception(&method(:handle_exception))
|
53
|
+
|
54
|
+
@deliver_session.start
|
55
|
+
@feedback_session.start
|
56
|
+
end
|
57
|
+
|
58
|
+
def stop
|
59
|
+
@deliver_session.stop
|
60
|
+
@feedback_session.stop
|
61
|
+
end
|
62
|
+
|
63
|
+
def wait_stop
|
64
|
+
@deliver_session.wait_stop
|
65
|
+
@feedback_session.wait_stop
|
66
|
+
end
|
67
|
+
|
68
|
+
def deliver(notification)
|
69
|
+
@deliver_session.push(notification)
|
70
|
+
end
|
71
|
+
|
72
|
+
def handle_delivery_error(notification, err)
|
73
|
+
@on_delivery_error.call(notification, err) if @on_delivery_error
|
74
|
+
end
|
75
|
+
|
76
|
+
def handle_feedback(notification, err)
|
77
|
+
@on_feedback.call(err.device_token) if @on_feedback
|
78
|
+
end
|
79
|
+
|
80
|
+
def handle_exception(e)
|
81
|
+
@on_exception.call(e) if @on_exception
|
82
|
+
end
|
83
|
+
|
84
|
+
def handle_sent(notification)
|
85
|
+
@on_sent.call(notification) if @on_sent
|
86
|
+
end
|
87
|
+
|
88
|
+
def on_delivery_error(&block)
|
89
|
+
@on_delivery_error = block
|
90
|
+
end
|
91
|
+
|
92
|
+
[:feedback, :sent, :delivery_error, :exception].each do |event|
|
93
|
+
event_name = "on_#{event}"
|
94
|
+
define_method(event_name) do |&block|
|
95
|
+
instance_variable_set("@#{event_name}", block)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def on_exception(&block)
|
100
|
+
@on_exception = block
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
module Mameapns
|
5
|
+
|
6
|
+
class Connection
|
7
|
+
attr_accessor :ssl_cert, :ssl_cert_key, :ssl_cert_pass, :host, :port, :last_write
|
8
|
+
|
9
|
+
IDLE_PERIOD = 1800
|
10
|
+
|
11
|
+
def initialize(host, port, options={})
|
12
|
+
self.host = host
|
13
|
+
self.port = port
|
14
|
+
|
15
|
+
options.each do |k, v|
|
16
|
+
setter_name = "#{k}="
|
17
|
+
self.__send__(setter_name, v) if respond_to?(setter_name)
|
18
|
+
end
|
19
|
+
|
20
|
+
written
|
21
|
+
end
|
22
|
+
|
23
|
+
def connect
|
24
|
+
setup_ssl_context
|
25
|
+
@tcp_socket, @ssl_socket = connect_socket
|
26
|
+
end
|
27
|
+
|
28
|
+
def close
|
29
|
+
begin
|
30
|
+
@tcp_socket.close
|
31
|
+
@ssl_socket.close
|
32
|
+
rescue IOError
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def read(bytesize)
|
37
|
+
@ssl_socket.read(bytesize)
|
38
|
+
end
|
39
|
+
|
40
|
+
def select(timeout)
|
41
|
+
IO.select([@ssl_socket], nil, nil, timeout)
|
42
|
+
end
|
43
|
+
|
44
|
+
def reconnect
|
45
|
+
close
|
46
|
+
@tcp_socket, @ssl_socket = connect_socket
|
47
|
+
end
|
48
|
+
|
49
|
+
def write(data)
|
50
|
+
reconnect if idle_period_exceeded?
|
51
|
+
|
52
|
+
retry_count = 0
|
53
|
+
|
54
|
+
begin
|
55
|
+
write_data(data)
|
56
|
+
rescue Errno::EPIPE, Errno::ETIMEOUT, OpenSSL::SSL::SSLError => e
|
57
|
+
retry_count += 1
|
58
|
+
|
59
|
+
if retry_count <= 3
|
60
|
+
reconnect
|
61
|
+
sleep 1
|
62
|
+
retry
|
63
|
+
else
|
64
|
+
raise ConnectionError, "Attempt to reconnect but failed"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def write_data(data)
|
70
|
+
@ssl_socket.write(data)
|
71
|
+
@ssl_socket.flush
|
72
|
+
written
|
73
|
+
end
|
74
|
+
|
75
|
+
def idle_period_exceeded?
|
76
|
+
Time.now - last_write > IDLE_PERIOD
|
77
|
+
end
|
78
|
+
|
79
|
+
def written
|
80
|
+
self.last_write = Time.now
|
81
|
+
end
|
82
|
+
|
83
|
+
def setup_ssl_context
|
84
|
+
begin
|
85
|
+
@ssl_context = OpenSSL::SSL::SSLContext.new
|
86
|
+
@ssl_context.key = OpenSSL::PKey::RSA.new(ssl_cert_key, ssl_cert_pass)
|
87
|
+
@ssl_context.cert = OpenSSL::X509::Certificate.new(ssl_cert)
|
88
|
+
@ssl_context
|
89
|
+
rescue OpenSSL::PKey::RSAError => e
|
90
|
+
raise SSLError, "Neither PUB key nor PRIV key:: nested asn1 error"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def connect_socket
|
95
|
+
begin
|
96
|
+
tcp_socket = TCPSocket.new(host, port)
|
97
|
+
tcp_socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, 1)
|
98
|
+
tcp_socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
99
|
+
ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, @ssl_context)
|
100
|
+
ssl_socket.sync = true
|
101
|
+
ssl_socket.connect
|
102
|
+
[tcp_socket, ssl_socket]
|
103
|
+
rescue OpenSSL::SSL::SSLError => e
|
104
|
+
raise SSLError, e
|
105
|
+
rescue SocketError => e
|
106
|
+
raise OpenConnectionError, e
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Mameapns
|
2
|
+
class Error < StandardError; end
|
3
|
+
|
4
|
+
# Connection error
|
5
|
+
# never succeed to send notificaiont while
|
6
|
+
# resolve under problems.
|
7
|
+
class ConnectionError < Error; end
|
8
|
+
class SSLError < ConnectionError; end
|
9
|
+
|
10
|
+
# Delivery error
|
11
|
+
class DeliveryError < Error
|
12
|
+
attr_accessor :code, :description
|
13
|
+
|
14
|
+
def initialize(code, description)
|
15
|
+
@code, @description = code, description
|
16
|
+
super(description)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
class DisconnectionError < Error; end
|
20
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Mameapns
|
2
|
+
class InterruptibleSleep
|
3
|
+
def wait(seconds)
|
4
|
+
@l, @r = IO.pipe
|
5
|
+
IO.select([@l], nil, nil, @seconds)
|
6
|
+
@l.close rescue IOError
|
7
|
+
@r.close rescue IOError
|
8
|
+
end
|
9
|
+
|
10
|
+
def interrupt
|
11
|
+
if @r
|
12
|
+
@r.close rescue IOError
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Mameapns
|
2
|
+
class Notification
|
3
|
+
include Options
|
4
|
+
|
5
|
+
options :axion_id
|
6
|
+
options :medium_id
|
7
|
+
|
8
|
+
options :identifier, default: 0
|
9
|
+
options :expiry, default: 86400
|
10
|
+
options :device_token
|
11
|
+
options :badge
|
12
|
+
options :sound, default: "1.aiff"
|
13
|
+
options :alert
|
14
|
+
options :attributes_for_device
|
15
|
+
options :related_infomation
|
16
|
+
|
17
|
+
def device_token=(token)
|
18
|
+
if !token.nil?
|
19
|
+
@device_token = token.gsub(/[ <>]/, "")
|
20
|
+
else
|
21
|
+
@device_token = nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def as_json
|
26
|
+
json = {}
|
27
|
+
json['aps'] = {}
|
28
|
+
json['aps']['alert'] = alert if alert
|
29
|
+
json['aps']['badge'] = badge if badge
|
30
|
+
json['aps']['sound'] = sound if sound
|
31
|
+
json['aps'].merge!(attributes_for_device) if attributes_for_device
|
32
|
+
json
|
33
|
+
end
|
34
|
+
|
35
|
+
def payload
|
36
|
+
as_json.to_json
|
37
|
+
end
|
38
|
+
|
39
|
+
def payload_size
|
40
|
+
payload.bytesize
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_binary
|
44
|
+
[1, identifier, expiry, 0, 32, device_token, 0, payload_size, payload].pack("cNNccH*cca*")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Mameapns
|
2
|
+
module Options
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(values={})
|
8
|
+
# like a sybolize_keys in active support
|
9
|
+
values.keys.each { |k| values[k.to_sym] = values.delete(k) }
|
10
|
+
|
11
|
+
self.class.attrs.each do |attr_name, option|
|
12
|
+
if values.keys.include?(attr_name)
|
13
|
+
self.__send__("#{attr_name}=", values[attr_name])
|
14
|
+
else
|
15
|
+
if option[:default]
|
16
|
+
self.__send__("#{attr_name}=", option[:default])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_hash
|
23
|
+
hash = {}
|
24
|
+
self.class.attrs.each do |attr_name, option|
|
25
|
+
value = self.send(attr_name)
|
26
|
+
if value.respond_to?(:to_hash)
|
27
|
+
hash[attr_name.to_s] = value.to_hash
|
28
|
+
else
|
29
|
+
hash[attr_name.to_s] = value
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
hash
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_json
|
37
|
+
to_hash.to_json
|
38
|
+
end
|
39
|
+
|
40
|
+
alias :initialize_options :initialize
|
41
|
+
|
42
|
+
module ClassMethods
|
43
|
+
def attrs
|
44
|
+
@attrs ||= {}
|
45
|
+
end
|
46
|
+
|
47
|
+
def options(name, option={})
|
48
|
+
class_eval do
|
49
|
+
attr_accessor name
|
50
|
+
end
|
51
|
+
|
52
|
+
attrs[name] = option
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module Mameapns
|
2
|
+
class Session
|
3
|
+
attr_accessor :connection, :ssl_cert, :ssl_cert_key, :ssl_cert_pass, :host, :port, :queue, :interruptible_sleep
|
4
|
+
|
5
|
+
Dir["#{File.expand_path("../", __FILE__)}/session/*.rb"].each do |file|
|
6
|
+
require file
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(options)
|
10
|
+
options.each do |k, v|
|
11
|
+
setter_name = "#{k}="
|
12
|
+
self.__send__(setter_name, v) if respond_to?(setter_name)
|
13
|
+
end
|
14
|
+
|
15
|
+
@connection ||= Connection.new(host, port,
|
16
|
+
ssl_cert: ssl_cert,
|
17
|
+
ssl_cert_key: ssl_cert_key,
|
18
|
+
ssl_cert_pass: ssl_cert_pass,
|
19
|
+
)
|
20
|
+
|
21
|
+
@on_exception = nil
|
22
|
+
@on_error = nil
|
23
|
+
|
24
|
+
@thread = nil
|
25
|
+
@queue = Queue.new
|
26
|
+
@running = false
|
27
|
+
@interruptible_sleep = InterruptibleSleep.new
|
28
|
+
end
|
29
|
+
|
30
|
+
def start
|
31
|
+
# connect to apns.
|
32
|
+
@running = true
|
33
|
+
|
34
|
+
@thread = Thread.start do
|
35
|
+
begin
|
36
|
+
start_session
|
37
|
+
rescue => e
|
38
|
+
@on_exception.call(e) if @on_exception
|
39
|
+
retry
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def start_session
|
45
|
+
# expect to implement by sub class
|
46
|
+
end
|
47
|
+
|
48
|
+
def end_session
|
49
|
+
# expect to implement by sub class
|
50
|
+
end
|
51
|
+
|
52
|
+
def handle_error(notification, err)
|
53
|
+
@on_error.call(notification, err) if @on_error
|
54
|
+
end
|
55
|
+
|
56
|
+
def handle_sent(notification)
|
57
|
+
@on_sent.call(notification) if @on_sent
|
58
|
+
end
|
59
|
+
|
60
|
+
def stop
|
61
|
+
@queue << nil
|
62
|
+
@running = false
|
63
|
+
@interruptible_sleep.interrupt
|
64
|
+
sleep 1 # FIXME: magic number
|
65
|
+
end
|
66
|
+
|
67
|
+
def wait_stop
|
68
|
+
@thread.join
|
69
|
+
end
|
70
|
+
|
71
|
+
def running?
|
72
|
+
!!@running
|
73
|
+
end
|
74
|
+
|
75
|
+
def push(data)
|
76
|
+
@queue << data
|
77
|
+
end
|
78
|
+
|
79
|
+
def select(wait_time)
|
80
|
+
@connection.select(wait_time)
|
81
|
+
end
|
82
|
+
|
83
|
+
def read(size)
|
84
|
+
@connection.read(size)
|
85
|
+
end
|
86
|
+
|
87
|
+
def write(data)
|
88
|
+
@connection.write(data)
|
89
|
+
end
|
90
|
+
|
91
|
+
def connect
|
92
|
+
@connection.connect
|
93
|
+
end
|
94
|
+
|
95
|
+
def close
|
96
|
+
@connection.close
|
97
|
+
end
|
98
|
+
|
99
|
+
def reconnect
|
100
|
+
@connection.reconnect
|
101
|
+
end
|
102
|
+
|
103
|
+
def on_exception(&block)
|
104
|
+
@on_exception = block
|
105
|
+
end
|
106
|
+
|
107
|
+
def on_error(&block)
|
108
|
+
@on_error = block
|
109
|
+
end
|
110
|
+
|
111
|
+
def on_sent(&block)
|
112
|
+
@on_sent = block
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
module Mameapns
|
3
|
+
class Session
|
4
|
+
class Deliver < Session
|
5
|
+
SELECT_TIMEOUT = 0.5
|
6
|
+
ERROR_TUPLE_BYTES = 6
|
7
|
+
|
8
|
+
APN_ERRORS = {
|
9
|
+
1 => "Processing error",
|
10
|
+
2 => "Missing device token",
|
11
|
+
3 => "Missing topic",
|
12
|
+
4 => "Missing payload",
|
13
|
+
5 => "Missing token size",
|
14
|
+
6 => "Missing topic size",
|
15
|
+
7 => "Missing payload size",
|
16
|
+
8 => "Invalid token",
|
17
|
+
255 => "None (unknown error)"
|
18
|
+
}
|
19
|
+
|
20
|
+
def start_session
|
21
|
+
connect
|
22
|
+
|
23
|
+
while notification = queue.pop
|
24
|
+
deliver(notification)
|
25
|
+
end
|
26
|
+
|
27
|
+
close
|
28
|
+
end
|
29
|
+
|
30
|
+
def deliver(notification)
|
31
|
+
write(notification.to_binary)
|
32
|
+
|
33
|
+
check_for_error(notification)
|
34
|
+
|
35
|
+
handle_sent(notification)
|
36
|
+
end
|
37
|
+
|
38
|
+
def check_for_error(notification)
|
39
|
+
if select(SELECT_TIMEOUT)
|
40
|
+
if tuple = read(ERROR_TUPLE_BYTES)
|
41
|
+
cmd, code, notification_id = tuple.unpack("ccN")
|
42
|
+
|
43
|
+
description = APN_ERRORS[code.to_i] || "Unknown error."
|
44
|
+
handle_error(notification, Mameapns::DeliveryError.new(code, description))
|
45
|
+
else
|
46
|
+
handle_error(notification, Mameapns::DisconnectionError.new)
|
47
|
+
end
|
48
|
+
|
49
|
+
begin
|
50
|
+
# エラーをうけとったらreconnectする。
|
51
|
+
reconnect
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Mameapns
|
2
|
+
class Session
|
3
|
+
class Feedback < Session
|
4
|
+
|
5
|
+
TAPLE_BYTES = 38
|
6
|
+
POLL = 60
|
7
|
+
|
8
|
+
def start_session
|
9
|
+
while running?
|
10
|
+
connect
|
11
|
+
|
12
|
+
while tuple = read(TAPLE_BYTES)
|
13
|
+
timestamp, _, device_token = tuple.unpack('N1n1H*')
|
14
|
+
handler_error(nil, DeviceNotExist.new(timestamp, device_token))
|
15
|
+
end
|
16
|
+
|
17
|
+
close
|
18
|
+
|
19
|
+
interruptible_sleep.wait(POLL)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/mameapns.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'mameapns/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "mameapns"
|
8
|
+
gem.version = Mameapns::VERSION
|
9
|
+
gem.authors = ["sutetotanuki"]
|
10
|
+
gem.email = ["sutetotanuki@gmail.com"]
|
11
|
+
gem.description = "mame apns"
|
12
|
+
gem.summary = "mame apns"
|
13
|
+
gem.homepage = ""
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
gem.add_runtime_dependency("thor")
|
20
|
+
gem.add_development_dependency("rspec")
|
21
|
+
gem.add_development_dependency("guard-rspec")
|
22
|
+
gem.add_development_dependency("growl")
|
23
|
+
gem.add_development_dependency("rb-fsevent", "~> 0.9.1")
|
24
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Mameapns
|
4
|
+
describe Notification do
|
5
|
+
it "should strip chevrons from the given string" do
|
6
|
+
notification = Mameapns::Notification.new(device_token: "<aa>")
|
7
|
+
notification.device_token.should eq "aa"
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should strip spaces from the given string" do
|
11
|
+
notification = Mameapns::Notification.new(device_token: "a a a")
|
12
|
+
notification.device_token.should eq "aaa"
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should default the sound to 1.aiff" do
|
16
|
+
Mameapns::Notification.new.sound.should eq "1.aiff"
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should default the expiry to 1 day" do
|
20
|
+
Mameapns::Notification.new.expiry.should eq 86400
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "#as_json" do
|
24
|
+
it "should include the alert if present" do
|
25
|
+
notification = Mameapns::Notification.new(alert: "aaa")
|
26
|
+
notification.as_json["aps"]["alert"].should eq "aaa"
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should not include the alert key if the alert is not present" do
|
30
|
+
notification = Mameapns::Notification.new
|
31
|
+
notification.as_json["aps"].key?("alert").should be_false
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should include the badge if present" do
|
35
|
+
notification = Mameapns::Notification.new(badge: 6)
|
36
|
+
notification.as_json["aps"]["badge"].should eq 6
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should not include the badge key if the badge is not present" do
|
40
|
+
notification = Mameapns::Notification.new
|
41
|
+
notification.as_json["aps"].key?("badge").should be_false
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should include attributes for the device" do
|
45
|
+
notification = Mameapns::Notification.new
|
46
|
+
notification.attributes_for_device = { "koko" => "hore", "wan" => "wan" }
|
47
|
+
notification.as_json["aps"]["koko"].should eq "hore"
|
48
|
+
notification.as_json["aps"]["wan"].should eq "wan"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "#to_binary" do
|
53
|
+
it "should correctly convert the notification to binary" do
|
54
|
+
notification = Mameapns::Notification.new({
|
55
|
+
identifier: 37,
|
56
|
+
alert: "abc",
|
57
|
+
device_token: "79e9b418e64ee99c4236a2cf5270e6b3421b8e2672a97670888829abe529c5e4"
|
58
|
+
})
|
59
|
+
|
60
|
+
notification.to_binary.should eq "\x01\x00\x00\x00%\x00\x01Q\x80\x00 y\xE9\xB4\x18\xE6N\xE9\x9CB6\xA2\xCFRp\xE6\xB3B\e\x8E&r\xA9vp\x88\x88)\xAB\xE5)\xC5\xE4\x00({\"aps\":{\"alert\":\"abc\",\"sound\":\"1.aiff\"}}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mameapns
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- sutetotanuki
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-12-18 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: thor
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: guard-rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: growl
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: rb-fsevent
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 0.9.1
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 0.9.1
|
94
|
+
description: mame apns
|
95
|
+
email:
|
96
|
+
- sutetotanuki@gmail.com
|
97
|
+
executables:
|
98
|
+
- mameapns
|
99
|
+
extensions: []
|
100
|
+
extra_rdoc_files: []
|
101
|
+
files:
|
102
|
+
- .gitignore
|
103
|
+
- Gemfile
|
104
|
+
- Guardfile
|
105
|
+
- LICENSE.txt
|
106
|
+
- README.md
|
107
|
+
- Rakefile
|
108
|
+
- bin/mameapns
|
109
|
+
- lib/mameapns.rb
|
110
|
+
- lib/mameapns/application.rb
|
111
|
+
- lib/mameapns/connection.rb
|
112
|
+
- lib/mameapns/error.rb
|
113
|
+
- lib/mameapns/interruptible_sleep.rb
|
114
|
+
- lib/mameapns/notification.rb
|
115
|
+
- lib/mameapns/options.rb
|
116
|
+
- lib/mameapns/session.rb
|
117
|
+
- lib/mameapns/session/deliver.rb
|
118
|
+
- lib/mameapns/session/feedback.rb
|
119
|
+
- lib/mameapns/version.rb
|
120
|
+
- mameapns.gemspec
|
121
|
+
- spec/mameapns/notification_spec.rb
|
122
|
+
- spec/spec_helper.rb
|
123
|
+
homepage: ''
|
124
|
+
licenses: []
|
125
|
+
post_install_message:
|
126
|
+
rdoc_options: []
|
127
|
+
require_paths:
|
128
|
+
- lib
|
129
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
130
|
+
none: false
|
131
|
+
requirements:
|
132
|
+
- - ! '>='
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '0'
|
135
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
136
|
+
none: false
|
137
|
+
requirements:
|
138
|
+
- - ! '>='
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '0'
|
141
|
+
requirements: []
|
142
|
+
rubyforge_project:
|
143
|
+
rubygems_version: 1.8.23
|
144
|
+
signing_key:
|
145
|
+
specification_version: 3
|
146
|
+
summary: mame apns
|
147
|
+
test_files:
|
148
|
+
- spec/mameapns/notification_spec.rb
|
149
|
+
- spec/spec_helper.rb
|