lead_zeppelin 0.1.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 +18 -0
- data/Gemfile +2 -0
- data/LICENSE +22 -0
- data/README.md +71 -0
- data/Rakefile +2 -0
- data/lead_zeppelin.gemspec +20 -0
- data/lib/lead_zeppelin.rb +9 -0
- data/lib/lead_zeppelin/apns.rb +13 -0
- data/lib/lead_zeppelin/apns/application.rb +45 -0
- data/lib/lead_zeppelin/apns/client.rb +75 -0
- data/lib/lead_zeppelin/apns/error.rb +26 -0
- data/lib/lead_zeppelin/apns/gateway.rb +108 -0
- data/lib/lead_zeppelin/apns/logger.rb +29 -0
- data/lib/lead_zeppelin/apns/notification.rb +39 -0
- data/lib/lead_zeppelin/version.rb +3 -0
- metadata +109 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Geoloqi
|
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,71 @@
|
|
1
|
+
# LeadZeppelin
|
2
|
+
|
3
|
+
Fast, threaded client for the Apple Push Notification Service.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'lead_zeppelin'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install lead_zeppelin
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
Require the gem, set threads to throw exceptions (optional, but recommended for now):
|
22
|
+
|
23
|
+
require 'lead_zeppelin'
|
24
|
+
Thread.abort_on_exception = true
|
25
|
+
|
26
|
+
Instantiate a new client and configure it by adding the block of code to handle error responses, and . Provide client.on\_error _before adding applications_.
|
27
|
+
|
28
|
+
client = LeadZeppelin::APNS::Client.new do |c|
|
29
|
+
c.on_error do |error_response|
|
30
|
+
puts "Apple sent back an error response: #{error_response.inspect}"
|
31
|
+
end
|
32
|
+
|
33
|
+
# You can provide .p12 files too! p12: File.read('./yourapp.p12')
|
34
|
+
c.add_application :your_app_identifier, pem: File.read('./yourapp.pem')
|
35
|
+
end
|
36
|
+
|
37
|
+
# Add a poller to read messages via a method of your choosing:
|
38
|
+
|
39
|
+
# Poll every second, join parent (main) thread so it doesn't close
|
40
|
+
|
41
|
+
client.poll(1, join_parent_thread: true) do |c|
|
42
|
+
c.message :demoapp, 'f80d44bc73b4a856d9bcd63c2285e5190f8a7dcd8af34cfdf1f4a23cfd66423d', "testing!"
|
43
|
+
end
|
44
|
+
|
45
|
+
# Logging
|
46
|
+
|
47
|
+
LeadZeppelin#logger takes a Logger class:
|
48
|
+
|
49
|
+
require 'logger'
|
50
|
+
LeadZeppelin.logger = Logger.new(STDERR)
|
51
|
+
|
52
|
+
To watch the thread flow, pass an IO to LeadZeppelin#thread_logger (but not a Logger):
|
53
|
+
|
54
|
+
LeadZeppelin.thread_logger = STDERR
|
55
|
+
|
56
|
+
## Contributing
|
57
|
+
|
58
|
+
1. Fork it
|
59
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
60
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
61
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
62
|
+
5. Create new Pull Request
|
63
|
+
|
64
|
+
## TODO
|
65
|
+
|
66
|
+
* *TESTING*
|
67
|
+
* Way to handle errors asynchronously using a pool
|
68
|
+
* Performance and concurrency speedups
|
69
|
+
* Edge cases
|
70
|
+
* Documentation (code level and regular)
|
71
|
+
* Length checking for payload, possibly an auto truncating feature
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/lead_zeppelin/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Kyle Drake"]
|
6
|
+
gem.email = ["kyledrake@gmail.com"]
|
7
|
+
gem.description = %q{Thread-safe, multi-application APNS client}
|
8
|
+
gem.summary = %q{Thread-safe, multi-application APNS client that makes it easier to develop notification software for the APNS service.}
|
9
|
+
gem.homepage = "https://github.com/geoloqi/lead_zeppelin"
|
10
|
+
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
+
gem.name = "lead_zeppelin"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = LeadZeppelin::VERSION
|
17
|
+
gem.add_dependency 'multi_json'
|
18
|
+
gem.add_dependency 'json'
|
19
|
+
gem.add_dependency 'connection_pool'
|
20
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'socket'
|
3
|
+
require 'openssl'
|
4
|
+
require 'multi_json'
|
5
|
+
require 'connection_pool'
|
6
|
+
require 'timeout'
|
7
|
+
require 'securerandom'
|
8
|
+
require_relative './apns/application'
|
9
|
+
require_relative './apns/client'
|
10
|
+
require_relative './apns/gateway'
|
11
|
+
require_relative './apns/logger'
|
12
|
+
require_relative './apns/notification'
|
13
|
+
require_relative './apns/error'
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module LeadZeppelin
|
2
|
+
module APNS
|
3
|
+
class Application
|
4
|
+
CONNECTION_POOL_SIZE = 5
|
5
|
+
CONNECTION_POOL_TIMEOUT = 5
|
6
|
+
|
7
|
+
def initialize(name, opts={})
|
8
|
+
@name = name
|
9
|
+
@opts = opts
|
10
|
+
|
11
|
+
@ssl_context = OpenSSL::SSL::SSLContext.new
|
12
|
+
|
13
|
+
if opts[:p12]
|
14
|
+
pem = OpenSSL::PKCS12.new opts[:p12], opts[:p12_pass]
|
15
|
+
@ssl_context.cert = pem.certificate
|
16
|
+
@ssl_context.key = pem.key
|
17
|
+
elsif opts[:pem]
|
18
|
+
@ssl_context.cert = OpenSSL::X509::Certificate.new opts[:pem]
|
19
|
+
@ssl_context.key = OpenSSL::PKey::RSA.new opts[:pem], opts[:pem_pass]
|
20
|
+
else
|
21
|
+
raise ArgumentError, 'opts[:p12] or opts[:pem] required'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def connect
|
26
|
+
cp_args = {size: (@opts[:connection_pool_size] || CONNECTION_POOL_SIZE),
|
27
|
+
timeout: (@opts[:connection_pool_timeout] || CONNECTION_POOL_TIMEOUT)}
|
28
|
+
|
29
|
+
@gateway_connection_pool = ConnectionPool.new(cp_args) do
|
30
|
+
Gateway.new @ssl_context, (@opts[:gateway_opts] || {}).merge(error_block: @opts[:error_block])
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def message(device_id, message, opts={})
|
35
|
+
connect if @gateway_connection_pool.nil?
|
36
|
+
|
37
|
+
@gateway_connection_pool.with_connection do |gateway|
|
38
|
+
gateway.write Notification.new(device_id, message, opts)
|
39
|
+
end
|
40
|
+
|
41
|
+
true
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module LeadZeppelin
|
2
|
+
module APNS
|
3
|
+
class Client
|
4
|
+
CLIENT_THREADS = 10
|
5
|
+
DEFAULT_POLL_FREQUENCY = 1
|
6
|
+
|
7
|
+
def initialize(opts={}, &configure)
|
8
|
+
Logger.info "instantiating client with options: #{opts.inspect}"
|
9
|
+
Logger.thread 'c'
|
10
|
+
@semaphore = Mutex.new
|
11
|
+
@opts = opts
|
12
|
+
|
13
|
+
self.instance_eval &configure
|
14
|
+
|
15
|
+
# FIXME
|
16
|
+
@thread_count = Queue.new
|
17
|
+
(opts[:client_threads] || CLIENT_THREADS).times {|t| @thread_count << t}
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_accessor :applications
|
21
|
+
|
22
|
+
def on_error(&block)
|
23
|
+
@error_block = block
|
24
|
+
end
|
25
|
+
|
26
|
+
def poll(frequency=DEFAULT_POLL_FREQUENCY, opts={}, &block)
|
27
|
+
Logger.info 'creating polling thread'
|
28
|
+
Logger.thread 'p'
|
29
|
+
@polling_thread = Thread.new {
|
30
|
+
loop do
|
31
|
+
self.instance_eval &block
|
32
|
+
sleep frequency
|
33
|
+
end
|
34
|
+
|
35
|
+
Logger.thread 'polling thread running'
|
36
|
+
}
|
37
|
+
|
38
|
+
@polling_thread.join if opts[:join_parent_thread] == true
|
39
|
+
end
|
40
|
+
|
41
|
+
def hold_open_poll
|
42
|
+
Logger.info 'attaching current thread to polling thread'
|
43
|
+
@polling_thread.join
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_application(name, opts={})
|
47
|
+
Logger.info "adding application \"#{name}\""
|
48
|
+
Logger.thread 'a'
|
49
|
+
@semaphore.synchronize do
|
50
|
+
@applications ||= {}
|
51
|
+
@applications[name] = Application.new name, opts.merge(error_block: @error_block)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def remove_application(name)
|
56
|
+
Logger.info "removing application \"#{name}\""
|
57
|
+
Logger.thread 'r'
|
58
|
+
@semaphore.synchronize do
|
59
|
+
deleted = @applications.delete name
|
60
|
+
Logger.warn "removing application \"#{name}\" failed! Name may be invalid." if deleted.nil?
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def message(app_name, device_id, message, opts={})
|
65
|
+
Logger.debug "message: \"#{app_name}\", \"#{device_id}\", \"#{message}\""
|
66
|
+
Logger.thread 'm'
|
67
|
+
|
68
|
+
# FIXME
|
69
|
+
t = @thread_count
|
70
|
+
@applications[app_name].message device_id, message, opts
|
71
|
+
@thread_count << t
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module LeadZeppelin
|
2
|
+
module APNS
|
3
|
+
class ErrorResponse
|
4
|
+
CODES = {
|
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 :code, :identifier, :message, :notification
|
18
|
+
|
19
|
+
def initialize(packet, notification=nil)
|
20
|
+
command, @code, @identifier = packet.unpack 'ccA4'
|
21
|
+
@message = CODES[@code]
|
22
|
+
@notification = notification
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module LeadZeppelin
|
2
|
+
module APNS
|
3
|
+
class Gateway
|
4
|
+
HOST = 'gateway.push.apple.com'
|
5
|
+
PORT = 2195
|
6
|
+
DEFAULT_TIMEOUT = 10
|
7
|
+
DEFAULT_SELECT_WAIT = 0.5
|
8
|
+
|
9
|
+
def initialize(ssl_context, opts={})
|
10
|
+
Logger.thread 'g'
|
11
|
+
@semaphore = Mutex.new
|
12
|
+
@ssl_context = ssl_context
|
13
|
+
@opts = opts
|
14
|
+
|
15
|
+
connect
|
16
|
+
end
|
17
|
+
|
18
|
+
def connect
|
19
|
+
Logger.thread 's'
|
20
|
+
begin
|
21
|
+
timeout(@opts[:timeout] || DEFAULT_TIMEOUT) do
|
22
|
+
socket = TCPSocket.new((@opts[:apns_host] || HOST), (@opts[:apns_port] || PORT))
|
23
|
+
ssl_socket = OpenSSL::SSL::SSLSocket.new socket, @ssl_context
|
24
|
+
ssl_socket.sync_close = true # when ssl_socket is closed, make sure the regular socket closes too.
|
25
|
+
ssl_socket.connect
|
26
|
+
|
27
|
+
@semaphore.synchronize do
|
28
|
+
@socket = socket
|
29
|
+
@ssl_socket = ssl_socket
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
Logger.debug "gateway connection established"
|
34
|
+
|
35
|
+
rescue Errno::ETIMEDOUT, Timeout::Error
|
36
|
+
Logger.warn "gateway connection timeout, retrying"
|
37
|
+
retry
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def reconnect
|
42
|
+
Logger.info "reconnecting to gateway"
|
43
|
+
Logger.thread 'r'
|
44
|
+
disconnect
|
45
|
+
connect
|
46
|
+
end
|
47
|
+
|
48
|
+
def disconnect
|
49
|
+
Logger.info "disconnecting from gateway"
|
50
|
+
Logger.thread 'd'
|
51
|
+
@ssl_socket.close
|
52
|
+
end
|
53
|
+
|
54
|
+
def process_error(notification=nil)
|
55
|
+
begin
|
56
|
+
error_response = @ssl_socket.read_nonblock 6
|
57
|
+
error = ErrorResponse.new error_response, notification
|
58
|
+
|
59
|
+
Logger.warn "error: #{error.code}, #{error.identifier.to_s}, #{error.message}"
|
60
|
+
Logger.thread 'e'
|
61
|
+
|
62
|
+
reconnect
|
63
|
+
|
64
|
+
if @opts[:error_block].nil? || !@opts[:error_block].respond_to?(:call)
|
65
|
+
Logger.error "You have not implemented an on_error block. This could lead to your account being banned from APNS. See the APNS docs"
|
66
|
+
else
|
67
|
+
@opts[:error_block].call(error)
|
68
|
+
end
|
69
|
+
|
70
|
+
rescue IO::WaitReadable
|
71
|
+
Logger.thread 'g'
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def write(notification)
|
76
|
+
Logger.thread 'w'
|
77
|
+
|
78
|
+
begin
|
79
|
+
process_error
|
80
|
+
|
81
|
+
@ssl_socket.write notification.payload
|
82
|
+
|
83
|
+
read, write, error = IO.select [@ssl_socket], [], [@ssl_socket], (@opts[:select_wait] || DEFAULT_SELECT_WAIT)
|
84
|
+
|
85
|
+
if !error.nil? && !error.first.nil?
|
86
|
+
Logger.error "IO.select has reported an unexpected error. Reconnecting, sleeping a bit and retrying"
|
87
|
+
sleep 1
|
88
|
+
reconnect
|
89
|
+
end
|
90
|
+
|
91
|
+
if !read.nil? && !read.first.nil?
|
92
|
+
process_error(notification)
|
93
|
+
return false
|
94
|
+
end
|
95
|
+
|
96
|
+
rescue Errno::EPIPE => e
|
97
|
+
Logger.warn 'gateway connection returned broken pipe, attempting reconnect and retrying'
|
98
|
+
Logger.thread 'f'
|
99
|
+
reconnect
|
100
|
+
retry
|
101
|
+
end
|
102
|
+
|
103
|
+
true
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module LeadZeppelin
|
2
|
+
module APNS
|
3
|
+
module Logger
|
4
|
+
|
5
|
+
module_function
|
6
|
+
|
7
|
+
def thread(string)
|
8
|
+
LeadZeppelin.thread_logger.print string.upcase if LeadZeppelin.thread_logger
|
9
|
+
end
|
10
|
+
|
11
|
+
def debug(string)
|
12
|
+
LeadZeppelin.logger.debug(string) if LeadZeppelin.logger
|
13
|
+
end
|
14
|
+
|
15
|
+
def info(string)
|
16
|
+
LeadZeppelin.logger.info(string) if LeadZeppelin.logger
|
17
|
+
end
|
18
|
+
|
19
|
+
def warn(string)
|
20
|
+
LeadZeppelin.logger.warn(string) if LeadZeppelin.logger
|
21
|
+
end
|
22
|
+
|
23
|
+
def error(string)
|
24
|
+
LeadZeppelin.logger.error(string) if LeadZeppelin.logger
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module LeadZeppelin
|
2
|
+
module APNS
|
3
|
+
class Notification
|
4
|
+
attr_reader :device_token, :identifier, :expiry
|
5
|
+
|
6
|
+
def initialize(device_token, message, opts={})
|
7
|
+
@device_token = device_token
|
8
|
+
@opts = opts
|
9
|
+
|
10
|
+
@identifier = @opts[:identifier] || SecureRandom.random_bytes(4)
|
11
|
+
@identifier = @identifier.to_s
|
12
|
+
|
13
|
+
@expiry = @opts[:expiry].nil? ? 1 : @opts[:expiry].to_i
|
14
|
+
|
15
|
+
if message.is_a?(Hash)
|
16
|
+
other = message.delete(:other)
|
17
|
+
@message = {aps: message}
|
18
|
+
@message.merge!(other) if other
|
19
|
+
elsif message.is_a?(String)
|
20
|
+
@message = {aps: {alert: message}}
|
21
|
+
else
|
22
|
+
raise ArgumentError, "notification message must be hash or string"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def payload
|
27
|
+
[1, @identifier, @expiry, 0, 32, packaged_token, 0, message_json.bytesize, message_json].pack("cA4Ncca*cca*")
|
28
|
+
end
|
29
|
+
|
30
|
+
def packaged_token
|
31
|
+
[device_token.gsub(/[\s|<|>]/,'')].pack('H*')
|
32
|
+
end
|
33
|
+
|
34
|
+
def message_json
|
35
|
+
@message_json ||= MultiJson.encode @message
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
metadata
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lead_zeppelin
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Kyle Drake
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-06-06 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: multi_json
|
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: json
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
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: connection_pool
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
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
|
+
description: Thread-safe, multi-application APNS client
|
63
|
+
email:
|
64
|
+
- kyledrake@gmail.com
|
65
|
+
executables: []
|
66
|
+
extensions: []
|
67
|
+
extra_rdoc_files: []
|
68
|
+
files:
|
69
|
+
- .gitignore
|
70
|
+
- Gemfile
|
71
|
+
- LICENSE
|
72
|
+
- README.md
|
73
|
+
- Rakefile
|
74
|
+
- lead_zeppelin.gemspec
|
75
|
+
- lib/lead_zeppelin.rb
|
76
|
+
- lib/lead_zeppelin/apns.rb
|
77
|
+
- lib/lead_zeppelin/apns/application.rb
|
78
|
+
- lib/lead_zeppelin/apns/client.rb
|
79
|
+
- lib/lead_zeppelin/apns/error.rb
|
80
|
+
- lib/lead_zeppelin/apns/gateway.rb
|
81
|
+
- lib/lead_zeppelin/apns/logger.rb
|
82
|
+
- lib/lead_zeppelin/apns/notification.rb
|
83
|
+
- lib/lead_zeppelin/version.rb
|
84
|
+
homepage: https://github.com/geoloqi/lead_zeppelin
|
85
|
+
licenses: []
|
86
|
+
post_install_message:
|
87
|
+
rdoc_options: []
|
88
|
+
require_paths:
|
89
|
+
- lib
|
90
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
91
|
+
none: false
|
92
|
+
requirements:
|
93
|
+
- - ! '>='
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
requirements: []
|
103
|
+
rubyforge_project:
|
104
|
+
rubygems_version: 1.8.24
|
105
|
+
signing_key:
|
106
|
+
specification_version: 3
|
107
|
+
summary: Thread-safe, multi-application APNS client that makes it easier to develop
|
108
|
+
notification software for the APNS service.
|
109
|
+
test_files: []
|