kw_apn 0.2
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/README.rdoc +51 -0
- data/Rakefile +16 -0
- data/VERSION +1 -0
- data/kw_apn.gemspec +19 -0
- data/lib/config.rb +40 -0
- data/lib/connection.rb +30 -0
- data/lib/core.rb +66 -0
- data/lib/feedback_reader.rb +26 -0
- data/lib/generators/kw_apn/kw_apn_generator.rb +18 -0
- data/lib/generators/kw_apn/templates/config/cert/dummy.pem +0 -0
- data/lib/generators/kw_apn/templates/config/kw_apn.yml +18 -0
- data/lib/generators/kw_apn/templates/log/kw_apn.log +0 -0
- data/lib/generators/kw_apn/usage +1 -0
- data/lib/kw_apn.rb +5 -0
- data/lib/sender.rb +145 -0
- metadata +80 -0
data/README.rdoc
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
====================================
|
|
2
|
+
Getting Started
|
|
3
|
+
====================================
|
|
4
|
+
For Rails start the generator: kw_apn
|
|
5
|
+
This will copy the configuration example into your project.
|
|
6
|
+
|
|
7
|
+
If you not working with Rails you need to specify either Rails.root and Rails.env or RACK_ROOT and RACK_ENV for the gem to work.
|
|
8
|
+
|
|
9
|
+
Manual:
|
|
10
|
+
create the File
|
|
11
|
+
<project_root>/config/kw_apn.yml
|
|
12
|
+
|
|
13
|
+
structure of the config file should look like this:
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
environment:
|
|
17
|
+
cert_file: path to ssl certificate file
|
|
18
|
+
push_host: 'gateway.sandbox.push.apple.com' or 'gateway.push.apple.com' for live
|
|
19
|
+
push_port: 2195
|
|
20
|
+
feedback_host: 'feedback.sandbox.push.apple.com' or 'feedback.push.apple.com' for live
|
|
21
|
+
feedback_port: 2196
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
====================================
|
|
25
|
+
Example
|
|
26
|
+
====================================
|
|
27
|
+
|
|
28
|
+
users = MyAPNUsers.all
|
|
29
|
+
n = []
|
|
30
|
+
payload = {:aps => {:alert => "Something very important for everyone to read", :sound => 'annoying_beep'}}
|
|
31
|
+
|
|
32
|
+
users.each do |u|
|
|
33
|
+
n << KwAPN::Notification.create(u.token, payload, 0)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
status, ret = KwAPN::Sender.push(n, 'TestSession')
|
|
37
|
+
|
|
38
|
+
if status == :ok
|
|
39
|
+
ret.each do |token|
|
|
40
|
+
MyAPNUsers.delete_all('token'=>token)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
====================================
|
|
46
|
+
Copyright
|
|
47
|
+
====================================
|
|
48
|
+
|
|
49
|
+
Distributed under the MIT License.
|
|
50
|
+
Based in part on Apns4r by Leonid Ponomarev (http://rdoc.info/projects/thegeekbird/Apns4r)
|
|
51
|
+
|
data/Rakefile
ADDED
data/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.2
|
data/kw_apn.gemspec
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
version = File.read('VERSION')
|
|
2
|
+
|
|
3
|
+
File.open(File.join(File.dirname(__FILE__), 'VERSION'), 'w') do |f|
|
|
4
|
+
f.write(version)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
Gem::Specification.new do |s|
|
|
8
|
+
s.name = 'kw_apn'
|
|
9
|
+
s.version = version
|
|
10
|
+
s.authors = ['Jonathan Cichon']
|
|
11
|
+
s.email = 'cichon@kupferwerk.com'
|
|
12
|
+
s.homepage = 'http://kupferwerk.com'
|
|
13
|
+
s.summary = 'APN Lib by Kupferwerk'
|
|
14
|
+
s.description = 'Apple Push Notification Library by Kupferwerk'
|
|
15
|
+
s.has_rdoc = true
|
|
16
|
+
s.extra_rdoc_files = ['README.rdoc']
|
|
17
|
+
s.require_path = 'lib'
|
|
18
|
+
s.files = Dir['lib/**/*'] + Dir['*.gemspec'] + ['Rakefile', 'README.rdoc', 'VERSION']
|
|
19
|
+
end
|
data/lib/config.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module KwAPN
|
|
2
|
+
require 'erb'
|
|
3
|
+
class Config
|
|
4
|
+
class << self
|
|
5
|
+
def options
|
|
6
|
+
@@options ||= nil
|
|
7
|
+
unless @@options
|
|
8
|
+
p_root = if defined? Rails
|
|
9
|
+
Rails.root
|
|
10
|
+
elsif defined? RACK_ROOT
|
|
11
|
+
RACK_ROOT
|
|
12
|
+
else
|
|
13
|
+
puts "Warning (KwAPN): You need to specifiy either Rails.root or RACK_ROOT for apns to work!"
|
|
14
|
+
nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
p_env = if defined? Rails
|
|
18
|
+
Rails.env
|
|
19
|
+
elsif defined? RACK_ENV
|
|
20
|
+
RACK_ENV
|
|
21
|
+
else
|
|
22
|
+
puts "Warning (KwAPN): You need to specifiy either Rails.env or RACK_ENV for apns to work!"
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
@@options = begin
|
|
27
|
+
raw_config = File.read(p_root.join("config", "kw_apn.yml"))
|
|
28
|
+
parsed_config = ERB.new(raw_config).result
|
|
29
|
+
YAML.load(parsed_config)[p_env].symbolize_keys
|
|
30
|
+
rescue => e
|
|
31
|
+
puts "Warning (KwAPN): Could not parse config file: #{e.message}"
|
|
32
|
+
{}
|
|
33
|
+
end
|
|
34
|
+
@@options[:root] = p_root
|
|
35
|
+
end
|
|
36
|
+
return @@options
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/connection.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module KwAPN
|
|
2
|
+
|
|
3
|
+
require 'socket'
|
|
4
|
+
require 'openssl'
|
|
5
|
+
|
|
6
|
+
class Connection
|
|
7
|
+
|
|
8
|
+
def connect(host, port, opts)
|
|
9
|
+
ctx = OpenSSL::SSL::SSLContext.new()
|
|
10
|
+
ctx.cert = OpenSSL::X509::Certificate.new(File::read(opts[:cert_file]))
|
|
11
|
+
ctx.key = OpenSSL::PKey::RSA.new(File::read(opts[:cert_file]))
|
|
12
|
+
|
|
13
|
+
s = TCPSocket.new(host, port)
|
|
14
|
+
ssl = OpenSSL::SSL::SSLSocket.new(s, ctx)
|
|
15
|
+
ssl.connect # start SSL session
|
|
16
|
+
ssl.sync_close = true # close underlying socket on SSLSocket#close
|
|
17
|
+
ssl
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
def log(s)
|
|
22
|
+
File.open(KwAPN::Config.options[:root].join("log", "kw_apn.log"), File::WRONLY|File::APPEND|File::CREAT, 0666) do |f|
|
|
23
|
+
f.write("#{s}\n")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
end
|
data/lib/core.rb
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
begin
|
|
2
|
+
require 'json'
|
|
3
|
+
rescue
|
|
4
|
+
puts "Warning: you need the json gem for apns to work!"
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
class Hash
|
|
8
|
+
MAX_PAYLOAD_LEN = 256
|
|
9
|
+
|
|
10
|
+
# Converts hash into JSON String.
|
|
11
|
+
# When payload is too long but can be chopped, tries to cut self.[:aps][:alert].
|
|
12
|
+
# If payload still don't fit Apple's restrictions, returns nil
|
|
13
|
+
#
|
|
14
|
+
# @return [String, nil] the object converted into JSON or nil.
|
|
15
|
+
def to_apn_payload
|
|
16
|
+
# Payload too long
|
|
17
|
+
if (to_json.length > MAX_PAYLOAD_LEN)
|
|
18
|
+
alert = self[:aps][:alert]
|
|
19
|
+
self[:aps][:alert] = ''
|
|
20
|
+
# can be chopped?
|
|
21
|
+
if (to_json.length > MAX_PAYLOAD_LEN)
|
|
22
|
+
return nil
|
|
23
|
+
else # inefficient way, but payload may be full of unicode-escaped chars, so...
|
|
24
|
+
self[:aps][:alert] = alert
|
|
25
|
+
while (self.to_json.length > MAX_PAYLOAD_LEN)
|
|
26
|
+
self[:aps][:alert].chop!
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
to_json
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Invokes {Hash#to_payload} and returns it's length
|
|
34
|
+
# @return [Fixnum, nil] length of object converted into JSON or nil.
|
|
35
|
+
def apn_payload_length
|
|
36
|
+
p = to_apn_payload
|
|
37
|
+
p ? p.length : nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
module KwAPN
|
|
43
|
+
|
|
44
|
+
class Notification
|
|
45
|
+
|
|
46
|
+
attr_accessor :identifier, :token
|
|
47
|
+
def initialize(token, payload, timestamp=0)
|
|
48
|
+
@token, @payload, @timestamp = token, payload, timestamp
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Creates new notification with given token and payload
|
|
52
|
+
# @param [String, Fixnum] token APNs token of device to notify
|
|
53
|
+
# @param [Hash, String] payload attached payload
|
|
54
|
+
def Notification.create(token, payload, timestamp=0)
|
|
55
|
+
Notification.new(token.kind_of?(String) ? token.delete(' ') : token.to_s(16) , payload.kind_of?(Hash) ? payload.to_apn_payload : payload, timestamp)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Converts to binary string wich can be writen directly into socket
|
|
59
|
+
# @return [String] binary string representation
|
|
60
|
+
def to_s
|
|
61
|
+
[1, @identifier, @timestamp, 32, @token, @payload.length, @payload].pack("CNNnH*na*")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module KwAPN
|
|
2
|
+
class FeedbackReader < Connection
|
|
3
|
+
|
|
4
|
+
attr_accessor :host, :port
|
|
5
|
+
def initialize(host=nil, port=nil)
|
|
6
|
+
@host = host || KwAPN::Config.options[:feedback_host]
|
|
7
|
+
@port = port || KwAPN::Config.options[:feedback_port]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def read
|
|
11
|
+
records ||= []
|
|
12
|
+
begin
|
|
13
|
+
@ssl = connect(@host, @port, KwAPN::Config.options)
|
|
14
|
+
while record = @ssl.read(38)
|
|
15
|
+
feedback = record.strip.unpack('NnH*')
|
|
16
|
+
records << feedback[2].scan(/.{0,8}/).join(' ').strip
|
|
17
|
+
end
|
|
18
|
+
rescue => e
|
|
19
|
+
puts "Error reading feedback channel: #{e.message}"
|
|
20
|
+
ensure
|
|
21
|
+
@ssl.close
|
|
22
|
+
end
|
|
23
|
+
return records
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require 'rails/generators'
|
|
2
|
+
|
|
3
|
+
class KwApnGenerator < Rails::Generators::Base
|
|
4
|
+
|
|
5
|
+
def install_kw_apn
|
|
6
|
+
source_paths << File.join(File.dirname(__FILE__), 'templates')
|
|
7
|
+
|
|
8
|
+
copy_file "config/kw_apn.yml"
|
|
9
|
+
directory "config/cert"
|
|
10
|
+
directory "log"
|
|
11
|
+
copy_file "log/kw_apn.log"
|
|
12
|
+
|
|
13
|
+
gem 'kw_apn'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
end
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
defaults: &defaults
|
|
2
|
+
cert_file: <%= Rails.root.join("config/cert", "apn_#{Rails.env}_cert.pem") %>
|
|
3
|
+
push_host: 'gateway.sandbox.push.apple.com'
|
|
4
|
+
push_port: 2195
|
|
5
|
+
feedback_host: 'feedback.sandbox.push.apple.com'
|
|
6
|
+
feedback_port: 2196
|
|
7
|
+
|
|
8
|
+
development:
|
|
9
|
+
<<: *defaults
|
|
10
|
+
profile: true
|
|
11
|
+
|
|
12
|
+
test:
|
|
13
|
+
<<: *defaults
|
|
14
|
+
|
|
15
|
+
production:
|
|
16
|
+
<<: *defaults
|
|
17
|
+
push_host: 'gateway.push.apple.com'
|
|
18
|
+
feedback_host: 'feedback.push.apple.com'
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Installs config templates to get you started with your project
|
data/lib/kw_apn.rb
ADDED
data/lib/sender.rb
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
module KwAPN
|
|
2
|
+
|
|
3
|
+
# maybe somebody knows why but Apple seems to have dificulties handling identifiers between 9 and ~ 20
|
|
4
|
+
# if there is a bug on our side feel free to fix it, but for now it seems to work with the offset workaround
|
|
5
|
+
ID_OFFSET = 333
|
|
6
|
+
class Sender < Connection
|
|
7
|
+
attr_accessor :host, :port, :count, :fail_count, :work_thread, :watch_thread, :failed_index_array, :session_id
|
|
8
|
+
|
|
9
|
+
# Creates new {Sender} object with given host and port
|
|
10
|
+
def initialize(session_id, host=nil, port=nil)
|
|
11
|
+
@session_id = session_id
|
|
12
|
+
@host = host || KwAPN::Config.options[:push_host] || 'gateway.sandbox.push.apple.com'
|
|
13
|
+
@port = port || KwAPN::Config.options[:push_port] || 2195
|
|
14
|
+
@count = 0
|
|
15
|
+
@fail_count = 0
|
|
16
|
+
@failed_index_array = []
|
|
17
|
+
self
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def push_batch(notifications=[])
|
|
21
|
+
begin
|
|
22
|
+
if @ssl != nil
|
|
23
|
+
return [:nok, 'Start a new Connection for every batch you want to perform']
|
|
24
|
+
end
|
|
25
|
+
@count = notifications.length
|
|
26
|
+
start_threads(notifications)
|
|
27
|
+
return [:ok, @failed_index_array.collect{|a| notifications[a].token}]
|
|
28
|
+
rescue => e
|
|
29
|
+
failed
|
|
30
|
+
self.class.log("(#{session_id}) Exception: #{e.message}")
|
|
31
|
+
return [:nok, "Exception: #{e.message}"]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def close_connection
|
|
36
|
+
@ssl.close if @ssl
|
|
37
|
+
@ssl = nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def start_threads(notifications, index=0)
|
|
43
|
+
@ssl = connect(@host, @port, KwAPN::Config.options)
|
|
44
|
+
if @ssl
|
|
45
|
+
@watch_thread = Thread.new do
|
|
46
|
+
perform_watch()
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
@work_thread = Thread.new do
|
|
50
|
+
perform_batch(notifications, index)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
@work_thread.join
|
|
54
|
+
|
|
55
|
+
if @failed_index_array.last and index <= @failed_index_array.last and @failed_index_array.last < @count - 1 and @ssl.nil?
|
|
56
|
+
# wait for apple to respond errors
|
|
57
|
+
# sleep(1)
|
|
58
|
+
start_threads(notifications, @failed_index_array.last + 1)
|
|
59
|
+
end
|
|
60
|
+
else
|
|
61
|
+
failed
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def perform_batch(notifications, index=0)
|
|
66
|
+
notifications[index..-1].each_with_index do |n, i|
|
|
67
|
+
begin
|
|
68
|
+
n.identifier = i + index + ID_OFFSET
|
|
69
|
+
bytes = @ssl.write(n.to_s)
|
|
70
|
+
if bytes <= 0
|
|
71
|
+
self.class.log("(#{session_id}) Warning at index #{i+index}: could not write to Socket")
|
|
72
|
+
# TODO?
|
|
73
|
+
# we do not realy want to respond to network errors, as we do not know how many apns might have been lost.
|
|
74
|
+
# At the moment we hope the watchthread does everything right and our connection holds.
|
|
75
|
+
end
|
|
76
|
+
rescue => e
|
|
77
|
+
# probably interrupted by watchthread, do nothing wait for restart
|
|
78
|
+
self.class.log("(#{session_id}) Exception at index #{i+index}: #{e.message}")
|
|
79
|
+
|
|
80
|
+
#if e.message == 'Broken pipe'
|
|
81
|
+
#end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
# wait for apple to respond errors
|
|
85
|
+
sleep(5)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def perform_watch
|
|
89
|
+
ret = @ssl.read
|
|
90
|
+
err = ret.strip.unpack('CCN')
|
|
91
|
+
if err[1] != 0 and err[2]
|
|
92
|
+
@failed_index_array << (err[2] - ID_OFFSET)
|
|
93
|
+
failed
|
|
94
|
+
@work_thread.exit
|
|
95
|
+
else
|
|
96
|
+
perform_watch
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def failed
|
|
101
|
+
@fail_count += 1
|
|
102
|
+
close_connection
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def error_msg(status)
|
|
106
|
+
case status
|
|
107
|
+
when 1
|
|
108
|
+
"Processing error"
|
|
109
|
+
when 2
|
|
110
|
+
"Missing device token"
|
|
111
|
+
when 3
|
|
112
|
+
"Missing topic"
|
|
113
|
+
when 4
|
|
114
|
+
"Missing payload"
|
|
115
|
+
when 5
|
|
116
|
+
"Invalid token size"
|
|
117
|
+
when 6
|
|
118
|
+
"Invalid topic size"
|
|
119
|
+
when 7
|
|
120
|
+
"Invalid payload size"
|
|
121
|
+
when 8
|
|
122
|
+
"Invalid token"
|
|
123
|
+
when 255
|
|
124
|
+
"unknown"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
class << self
|
|
129
|
+
# convenient way of connecting with apple and pushing the notifications
|
|
130
|
+
# @param [Array] notifications - An Array of Objects of Type KwAPN::Notification
|
|
131
|
+
# @param [String] session_id - A Identifier for Login purpose
|
|
132
|
+
#
|
|
133
|
+
# @returns [Symbol, Array/String] if no Problems occured :ok and an Array of Tokens failed to push is returned. The Caller should take care of those invalid Tokens.
|
|
134
|
+
def push(notifications, session_id=nil)
|
|
135
|
+
s = self.new(session_id)
|
|
136
|
+
startdate = Time.now
|
|
137
|
+
status, ret = s.push_batch(notifications)
|
|
138
|
+
log("(#{session_id}) #{startdate.to_s} SENT APN #{s.count - s.fail_count}/#{s.count} in #{Time.now.to_i - startdate.to_i} seconds")
|
|
139
|
+
s.close_connection
|
|
140
|
+
return [status, ret]
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
metadata
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: kw_apn
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
hash: 15
|
|
5
|
+
prerelease: false
|
|
6
|
+
segments:
|
|
7
|
+
- 0
|
|
8
|
+
- 2
|
|
9
|
+
version: "0.2"
|
|
10
|
+
platform: ruby
|
|
11
|
+
authors:
|
|
12
|
+
- Jonathan Cichon
|
|
13
|
+
autorequire:
|
|
14
|
+
bindir: bin
|
|
15
|
+
cert_chain: []
|
|
16
|
+
|
|
17
|
+
date: 2010-08-05 00:00:00 +02:00
|
|
18
|
+
default_executable:
|
|
19
|
+
dependencies: []
|
|
20
|
+
|
|
21
|
+
description: Apple Push Notification Library by Kupferwerk
|
|
22
|
+
email: cichon@kupferwerk.com
|
|
23
|
+
executables: []
|
|
24
|
+
|
|
25
|
+
extensions: []
|
|
26
|
+
|
|
27
|
+
extra_rdoc_files:
|
|
28
|
+
- README.rdoc
|
|
29
|
+
files:
|
|
30
|
+
- lib/config.rb
|
|
31
|
+
- lib/connection.rb
|
|
32
|
+
- lib/core.rb
|
|
33
|
+
- lib/feedback_reader.rb
|
|
34
|
+
- lib/generators/kw_apn/kw_apn_generator.rb
|
|
35
|
+
- lib/generators/kw_apn/templates/config/cert/dummy.pem
|
|
36
|
+
- lib/generators/kw_apn/templates/config/kw_apn.yml
|
|
37
|
+
- lib/generators/kw_apn/templates/log/kw_apn.log
|
|
38
|
+
- lib/generators/kw_apn/usage
|
|
39
|
+
- lib/kw_apn.rb
|
|
40
|
+
- lib/sender.rb
|
|
41
|
+
- kw_apn.gemspec
|
|
42
|
+
- Rakefile
|
|
43
|
+
- README.rdoc
|
|
44
|
+
- VERSION
|
|
45
|
+
has_rdoc: true
|
|
46
|
+
homepage: http://kupferwerk.com
|
|
47
|
+
licenses: []
|
|
48
|
+
|
|
49
|
+
post_install_message:
|
|
50
|
+
rdoc_options: []
|
|
51
|
+
|
|
52
|
+
require_paths:
|
|
53
|
+
- lib
|
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
55
|
+
none: false
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
hash: 3
|
|
60
|
+
segments:
|
|
61
|
+
- 0
|
|
62
|
+
version: "0"
|
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
64
|
+
none: false
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
hash: 3
|
|
69
|
+
segments:
|
|
70
|
+
- 0
|
|
71
|
+
version: "0"
|
|
72
|
+
requirements: []
|
|
73
|
+
|
|
74
|
+
rubyforge_project:
|
|
75
|
+
rubygems_version: 1.3.7
|
|
76
|
+
signing_key:
|
|
77
|
+
specification_version: 3
|
|
78
|
+
summary: APN Lib by Kupferwerk
|
|
79
|
+
test_files: []
|
|
80
|
+
|