suj-pusher 0.2.0 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -4,11 +4,11 @@ This is a simple but enterprise level pusher server that can push notifications
4
4
 
5
5
  ## Features
6
6
 
7
- - Support both APN and GCM push services with a simple unified API interface.
8
- - Keep persistent connections to APN following Apple recommendation.
9
- - Use redis pub/sub mechanism for real time push notifications. No polling.
7
+ - EventMachine based for efficient use of resources.
8
+ - Support both APN, GCM, WNS, and WPNS push services with a simple unified json API interface.
9
+ - Keep persistent connections to APN following Apple's recommendations.
10
+ - Uses the APN protocol version 2 for batched notifications.
10
11
  - No need to set APN certificates or GCM api keys in configuration files or pusher startup. These are sent in a per request basis. This allows support for multiple APN certs and GCM api keys in a single Pusher instance.
11
- - EventMachine based to handle in the order of thousands of push requests per second.
12
12
 
13
13
  ## Installation
14
14
 
@@ -113,15 +113,8 @@ When sending push notifications to iOS devices you must provide an aps hash insi
113
113
 
114
114
  Read the [official documentation](http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW1) for details on the *aps* hash format. Note that this hash must not exceed the 256 bytes or it will be rejected by the APN service.
115
115
 
116
- #### Sending one message to both APN and GCM
117
-
118
- Normally you would send messages to either Android or iOS indenpendently. But the pusher daemon can send the same message to devices on both networks as long as you follow the Apple restrictions. This is because Apple push messages are more limited than GCM.
119
-
120
- If your data hash is compatible with the APN standard as described above and you specify a APN cert, a GCM api_key, a list of apn_ids and a list of gcm_ids then the message will be delivered via push notification to all the devices in those lists. Apple will display the notifications using their own mechanisms and for Android you will receive the data hash in a Bundle object as usual. Is your responsibility to extract the data from that bundle and display/use it as you please.
121
-
122
-
123
-
124
116
  #### Sending one message to WNS
117
+
125
118
  message hash: {
126
119
  wnstype: "type" ,
127
120
  wnsrequeststatus: true,
@@ -136,7 +129,6 @@ secret and sid: App identification credentials provided by microsoft when regist
136
129
  wnsrequeststatus: boolean, if true, the response from wns server will have aditional information
137
130
  wnsids: jsonArray of target devices.
138
131
 
139
-
140
132
  #### Sending one message to WPNS
141
133
  message hash: { wptype: "type",
142
134
  wpids: ["xxx"],
@@ -150,8 +142,6 @@ secret: a unic hash to identify a conection, internal use, each notification sen
150
142
  wpids: jsonArray of ids for target devices
151
143
  data: notification data to send, please use de xsml template provided by Microsoft(http://msdn.microsoft.com/en-us/library/windowsphone/develop/hh202945(v=vs.105).aspx) for each wptype listed above
152
144
 
153
-
154
-
155
145
  ## Examples
156
146
 
157
147
  A simple example using ruby code to send a push notification to iOS devices.
@@ -181,11 +171,9 @@ redis = Redis.new({ host: "localhost", port: 6379})
181
171
  # Push the message to the *suj_pusher_queue* in the redis server:
182
172
  redis.lpush "pusher:suj_pusher_msgs", msg_json
183
173
 
184
- # Notify workers there is a new message
185
- redis.publish "pusher:suj_pusher_queue", "PUSH_MSG"
186
174
  ```
187
175
 
188
- First you must push your messages to the *suj_pusher_msgs* queue and then publish a *PUSH_MSG* to the *suj_pusher_queue*. The first is a simple queue that stores the messages and the second is a PUB/SUB queue that tells the push daemons that a new message is available. Also make sure to set the same namespace (e.g. pusher) to the queue names that you set on the pusher daemons.
176
+ Once you push the JSON message to the *suj_pusher_msgs* queue the pusher workers will retrieve and process it.
189
177
 
190
178
  ## Issues
191
179
 
data/bin/pusher CHANGED
@@ -57,6 +57,7 @@ Suj::Pusher.config.update(config)
57
57
  PusherDaemon.spawn!(
58
58
  sync_log: true,
59
59
  working_dir: Dir.pwd,
60
+ processes: 1,
60
61
  log_file: File.join(logdir, "pusher-worker.log"),
61
62
  pid_file: File.join(piddir, "pusher-worker.pid")
62
63
  )
data/lib/suj/pusher.rb CHANGED
@@ -17,8 +17,6 @@ require 'logger'
17
17
 
18
18
  module Suj
19
19
  module Pusher
20
-
21
- QUEUE = "suj_pusher_queue"
22
20
  MSG_QUEUE = "suj_pusher_msgs"
23
21
  end
24
22
  end
@@ -1,4 +1,5 @@
1
1
  require "eventmachine"
2
+ require "iobuffer"
2
3
 
3
4
  require "base64"
4
5
  module Suj
@@ -16,6 +17,7 @@ module Suj
16
17
  6 => "Invalid topic size",
17
18
  7 => "Invalid payload size",
18
19
  8 => "Invalid token",
20
+ 10 => "Shutdown",
19
21
  255 => "Unknown error"
20
22
  }
21
23
 
@@ -26,6 +28,7 @@ module Suj
26
28
  @options = options
27
29
  @cert_key = Digest::SHA1.hexdigest(@options[:cert])
28
30
  @cert_file = File.join(Suj::Pusher.config.certs_path, @cert_key)
31
+ @buffer = IO::Buffer.new
29
32
  File.open(@cert_file, "w") do |f|
30
33
  f.write @options[:cert]
31
34
  end
@@ -42,18 +45,22 @@ module Suj
42
45
 
43
46
  def deliver(data)
44
47
  begin
45
- @notification = Suj::Pusher::ApnNotification.new(data)
48
+ @notifications = []
49
+ data[:apn_ids].each do |apn_id|
50
+ @notifications << Suj::Pusher::ApnNotification.new(data.merge({token: apn_id}))
51
+ end
46
52
  if ! disconnected?
47
53
  info "APN delivering data"
48
- send_data(@notification.data)
49
- @notification = nil
54
+ send_data(@notifications.join)
55
+ info "APN push notification sent"
56
+ @notifications = nil
50
57
  info "APN delivered data"
51
58
  else
52
59
  info "APN connection unavailable"
53
60
  end
54
61
  rescue Suj::Pusher::ApnNotification::PayloadTooLarge => e
55
62
  error "APN notification payload too large."
56
- debug @notification.data.inspect
63
+ debug @notifications.join.inspect
57
64
  rescue => ex
58
65
  error "APN notification error : #{ex}"
59
66
  end
@@ -64,22 +71,34 @@ module Suj
64
71
  start_tls(@ssl_options)
65
72
  end
66
73
 
74
+ # Receives error data from APN servers. Each error is 6 bytes long
75
+ # and contains:
76
+ #
77
+ # cmd -> 1 byte unsigned integer that is always 8
78
+ # status -> 1 byte unsigned integer that indicates the error
79
+ # See ERRORS array for a list
80
+ # id -> 4 byte message ID set when the message was sent
67
81
  def receive_data(data)
68
- cmd, status, id = data.unpack("ccN")
69
- if status != 0
70
- error "APN push error received: #{ERRORS[status]}"
71
- else
72
- info "APN push notification sent"
82
+ @buffer << data
83
+ while @buffer.size >= 6
84
+ res = @buffer.read(6)
85
+ cmd, status, id = data.unpack("CCN")
86
+ if cmd != 8
87
+ error "APN push response command differs from 8"
88
+ elsif status != 0
89
+ error "APN push error received: #{ERRORS[status]} for id #{id}"
90
+ end
73
91
  end
74
92
  end
75
93
 
76
94
  def connection_completed
77
95
  info "APN Connection established..."
78
96
  @disconnected = false
79
- if ! @notification.nil?
97
+ if ! @notifications.nil?
80
98
  info "EST - APN delivering data"
81
- send_data(@notification.data)
82
- @notification = nil
99
+ send_data(@notifications.join)
100
+ info "APN push notification sent"
101
+ @notifications = nil
83
102
  info "EST - APN delivered data"
84
103
  end
85
104
  end
@@ -87,7 +106,6 @@ module Suj
87
106
  def unbind
88
107
  info "APN Connection closed..."
89
108
  @disconnected = true
90
- FileUtils.rm_f(@cert_file)
91
109
  @pool.remove_connection(@cert_key)
92
110
  end
93
111
  end
@@ -1,4 +1,5 @@
1
1
  require "eventmachine"
2
+ require "iobuffer"
2
3
 
3
4
  require "base64"
4
5
  module Suj
@@ -6,12 +7,15 @@ module Suj
6
7
  class APNFeedbackConnection < EM::Connection
7
8
  include Suj::Pusher::Logger
8
9
 
9
- def initialize(options = {})
10
+ def initialize(pool, options = {})
10
11
  super
11
12
  @disconnected = true
12
13
  @options = options
13
- @cert_key = Digest::SHA1.hexdigest(@options[:cert])
14
+ @pool = pool
15
+ @cert_key = Digest::SHA1.hexdigest("FEEDBACK" + @options[:cert])
14
16
  @cert_file = File.join(Suj::Pusher.config.certs_path, @cert_key)
17
+ @buffer = IO::Buffer.new
18
+ self.comm_inactivity_timeout = 10 # Close after 10 sec of inactivity
15
19
  File.open(@cert_file, "w") do |f|
16
20
  f.write @options[:cert]
17
21
  end
@@ -31,9 +35,21 @@ module Suj
31
35
  start_tls(@ssl_options)
32
36
  end
33
37
 
38
+ # Receive feedback data from APN servers.
39
+ #
40
+ # The format is:
41
+ #
42
+ # timestamp -> 4 byte bigendian
43
+ # len -> 2 byte token length
44
+ # token -> 32 bytes token
34
45
  def receive_data(data)
35
- timestamp, size, token = data.unpack("QnN")
36
- info "APN Feedback invalid token #{token}"
46
+ @buffer << data
47
+
48
+ while @buffer.size >= 38
49
+ timestamp, size = @buffer.read(6).unpack("Nn")
50
+ token = @buffer.read(size)
51
+ puts " FEEDBACK TIMESTAMP: #{timestamp} SIZE: #{size} TOKEN: #{token}"
52
+ end
37
53
  end
38
54
 
39
55
  def connection_completed
@@ -44,6 +60,7 @@ module Suj
44
60
  def unbind
45
61
  info "APN Feedback Connection closed..."
46
62
  @disconnected = true
63
+ @pool.remove_connection(@cert_key)
47
64
  end
48
65
  end
49
66
  end
@@ -23,6 +23,10 @@ module Suj
23
23
  @data ||= encode_data
24
24
  end
25
25
 
26
+ def to_s
27
+ data
28
+ end
29
+
26
30
  private
27
31
 
28
32
  def get_expiry
@@ -34,12 +38,31 @@ module Suj
34
38
  end
35
39
 
36
40
  def encode_data
37
- identifier = 0
38
- expiry = get_expiry
39
- size = [payload].pack("a*").size
40
- data_array = [1, identifier, expiry, 32, @token, size, payload]
41
- info("PAYLOAD: #{data_array}")
42
- data_array.pack("cNNnH*na*")
41
+ # identifier = 0
42
+ # expiry = get_expiry
43
+ # size = [payload].pack("a*").size
44
+ # data_array = [1, identifier, expiry, 32, @token, size, payload]
45
+ # info("PAYLOAD: #{data_array}")
46
+ # data_array.pack("cNNnH*na*")
47
+
48
+ items = [
49
+ [1, 32, @token ], # token
50
+ [2, payload.bytesize, payload ], # payload
51
+ [3, 4, OpenSSL::Random::random_bytes(4) ], # random identifier
52
+ [4, 4, get_expiry ], # expiration date
53
+ [5, 1, 10 ] # high priority
54
+ ]
55
+
56
+ info("PAYLOAD: #{items}")
57
+
58
+ frame_data =
59
+ items[0].pack("CnH*") +
60
+ items[1].pack("CnA*") +
61
+ items[2].pack("CnA*") +
62
+ items[3].pack("CnN") +
63
+ items[4].pack("CnC")
64
+
65
+ [2,frame_data.bytesize,frame_data].pack("CNA*")
43
66
  end
44
67
  end
45
68
  end
@@ -1,4 +1,5 @@
1
1
  require "base64"
2
+ require "thread"
2
3
  require File.join File.dirname(File.expand_path(__FILE__)), "apn_connection.rb"
3
4
  require File.join File.dirname(File.expand_path(__FILE__)), "gcm_connection.rb"
4
5
 
@@ -17,61 +18,77 @@ module Suj
17
18
  def initialize(daemon)
18
19
  @pool = {}
19
20
  @daemon = daemon
21
+ @mutex = Mutex.new
20
22
  end
21
23
 
22
24
  def apn_connection(options = {})
23
25
  cert = Digest::SHA1.hexdigest options[:cert]
24
26
  info "APN connection #{cert}"
25
- @pool[cert] ||= EM.connect(APN_GATEWAY, APN_PORT, APNConnection, self, options)
27
+ @mutex.synchronize do
28
+ @pool[cert] ||= EM.connect(APN_GATEWAY, APN_PORT, APNConnection, self, options)
29
+ end
26
30
  end
27
31
 
28
32
  def apn_sandbox_connection(options = {})
29
33
  cert = Digest::SHA1.hexdigest options[:cert]
30
34
  info "APN connection #{cert}"
31
- @pool[cert] ||= EM.connect(APN_SANDBOX, APN_PORT, APNConnection, self, options)
35
+ @mutex.synchronize do
36
+ @pool[cert] ||= EM.connect(APN_SANDBOX, APN_PORT, APNConnection, self, options)
37
+ end
32
38
  end
33
39
 
34
40
  def feedback_connection(options = {})
35
- info "Feedback connection"
36
- EM.connect(FEEDBACK_GATEWAY, FEEDBACK_PORT, APNFeedbackConnection, options)
41
+ cert = Digest::SHA1.hexdigest("FEEDBACK" + options[:cert])
42
+ info "APN Feedback connection #{cert}"
43
+ @mutex.synchronize do
44
+ @pool[cert] ||= EM.connect(FEEDBACK_GATEWAY, FEEDBACK_PORT, APNFeedbackConnection, self, options)
45
+ end
37
46
  end
38
47
 
39
48
  def feedback_sandbox_connection(options = {})
40
- info "Feedback sandbox connection"
41
- EM.connect(FEEDBACK_SANDBOX, FEEDBACK_PORT, APNFeedbackConnection, options)
49
+ info "Feedback connection"
50
+ cert = Digest::SHA1.hexdigest("FEEDBACK" + options[:cert])
51
+ info "APN Sandbox Feedback connection #{cert}"
52
+ @mutex.synchronize do
53
+ @pool[cert] ||= EM.connect(FEEDBACK_SANDBOX, FEEDBACK_PORT, APNFeedbackConnection, self, options)
54
+ end
42
55
  end
43
56
 
44
57
  def gcm_connection(options = {})
45
58
  # All GCM connections are unique, even if they are to the same app.
46
59
  api_key = "#{options[:api_key]}#{rand * 100}"
47
60
  info "GCM connection #{api_key}"
48
- @pool[api_key] ||= Suj::Pusher::GCMConnection.new(self, api_key, options)
61
+ @mutex.synchronize do
62
+ @pool[api_key] ||= Suj::Pusher::GCMConnection.new(self, api_key, options)
63
+ end
49
64
  end
50
65
 
51
66
  def apn_connection(options = {})
52
67
  cert = Digest::SHA1.hexdigest options[:cert]
53
68
  info "APN connection #{cert}"
54
- @pool[cert] ||= EM.connect(APN_GATEWAY, APN_PORT, APNConnection, self, options)
69
+ @mutex.synchronize do
70
+ @pool[cert] ||= EM.connect(APN_GATEWAY, APN_PORT, APNConnection, self, options)
71
+ end
55
72
  end
56
73
 
57
74
  def wns_connection(options = {})
58
75
  cert = Digest::SHA1.hexdigest options[:secret]
59
76
  info "WNS connection #{cert}"
60
77
  info "WNS Options #{options}"
61
- @pool[cert] ||= Suj::Pusher::WNSConnection.new(self,options)
78
+ @mutex.synchronize do
79
+ @pool[cert] ||= Suj::Pusher::WNSConnection.new(self,options)
80
+ end
62
81
  end
63
82
 
64
83
  def wpns_connection(options = {})
65
84
  cert = Digest::SHA1.hexdigest options[:secret]
66
85
  info "WPNS connection #{cert}"
67
86
  info "WPNS Options #{options}"
68
- @pool[cert] ||= Suj::Pusher::WPNSConnection.new(self,options)
69
- return @pool[cert]
87
+ @mutex.synchronize do
88
+ @pool[cert] ||= Suj::Pusher::WPNSConnection.new(self,options)
89
+ end
70
90
  end
71
91
 
72
-
73
-
74
-
75
92
  def remove_connection(key)
76
93
  info "Removing connection #{key}"
77
94
  info "Connection not found" unless @pool.delete(key)
@@ -16,22 +16,10 @@ module Suj
16
16
  def start
17
17
  info "Starting pusher daemon"
18
18
  info " subsribe to push messages from #{redis_url} namespace #{redis_namespace}"
19
+ @last_feedback = Time.now
20
+ @last_sandbox_feedback = Time.now
19
21
  EM.run do
20
- wait_msg do |msg|
21
- begin
22
- info "RECEIVED MESSAGE"
23
- data = Hash.symbolize_keys(MultiJson.load(msg))
24
- send_notification(data)
25
- info "SENT MESSAGE"
26
- retrieve_feedback(data)
27
- info "FINISHED FEEDBACK RETRIEVAL"
28
- rescue MultiJson::LoadError
29
- warn("Received invalid json data, discarding msg")
30
- rescue => e
31
- error("Error sending notification : #{e}")
32
- error e.backtrace
33
- end
34
- end
22
+ wait_msg
35
23
  end
36
24
  end
37
25
 
@@ -47,28 +35,25 @@ module Suj
47
35
  private
48
36
 
49
37
  def wait_msg
50
- redis.on(:connected) { info "REDIS - Connected to Redis server #{redis_url}" }
51
- redis.on(:closed) { info "REDIS - Closed connection to Redis server" }
52
- redis.on(:failed) { info "REDIS - redis connection FAILED" }
53
- redis.on(:reconnected) { info "REDIS - Reconnected to Redis server" }
54
- redis.on(:disconnected) { info "REDIS - Disconnected from Redis server" }
55
- redis.on(:reconnect_failed) { info "REDIS - Reconnection attempt to Redis server FAILED" }
56
- # EM.add_periodic_timer(30) { redis.publish Suj::Pusher::QUEUE, "ECHO" }
57
- redis.pubsub.subscribe("#{redis_namespace}:#{Suj::Pusher::QUEUE}") do |msg|
58
- if msg == "ECHO"
59
- info "REDIS - ECHO Received"
60
- elsif msg == "PUSH_MSG"
61
- info "REDIS - PUSH_MSG Received"
62
- get_message.callback do |message|
63
- if message
64
- yield message
65
- else
66
- info "REDIS - PUSH_MSG Queue was empty"
67
- end
68
- end
69
- else
70
- yield msg
38
+ defer = redis.brpop "#{redis_namespace}:#{MSG_QUEUE}", 0
39
+ defer.callback do |_, msg|
40
+ begin
41
+ info "RECEIVED MESSAGE"
42
+ data = Hash.symbolize_keys(MultiJson.load(msg))
43
+ send_notification(data)
44
+ info "SENT MESSAGE"
45
+ retrieve_feedback(data)
46
+ rescue MultiJson::LoadError
47
+ warn("Received invalid json data, discarding msg")
48
+ rescue => e
49
+ error("Error sending notification : #{e}")
50
+ error e.backtrace
71
51
  end
52
+ EM.next_tick { wait_msg }
53
+ end
54
+ defer.errback do |e|
55
+ error e
56
+ EM.next_tick { wait_msg }
72
57
  end
73
58
  end
74
59
 
@@ -105,28 +90,30 @@ module Suj
105
90
  def send_apn_notification(msg)
106
91
  info "Sending APN notification via connection #{Digest::SHA1.hexdigest(msg[:cert])}"
107
92
  conn = pool.apn_connection(msg)
108
- msg[:apn_ids].each do |apn_id|
109
- conn.deliver(msg.merge({token: apn_id}))
110
- end
93
+ conn.deliver(msg)
94
+ # msg[:apn_ids].each do |apn_id|
95
+ # conn.deliver(msg.merge({token: apn_id}))
96
+ # end
111
97
  end
112
98
 
113
99
  def send_apn_sandbox_notification(msg)
114
100
  info "Sending APN sandbox notification via connection #{Digest::SHA1.hexdigest(msg[:cert])}"
115
101
  conn = pool.apn_sandbox_connection(msg)
116
- msg[:apn_ids].each do |apn_id|
117
- conn.deliver(msg.merge({token: apn_id}))
118
- end
102
+ conn.deliver(msg)
103
+ # msg[:apn_ids].each do |apn_id|
104
+ # conn.deliver(msg.merge({token: apn_id}))
105
+ # end
119
106
  end
120
107
 
121
108
  def feedback_connection(msg)
122
- return if @last_feedback and (Time.now - @last_feedback < FEEDBACK_TIME)
109
+ return Time.now - @last_feedback < FEEDBACK_TIME
123
110
  info "Get feedback information"
124
111
  conn = pool.feedback_connection(msg)
125
112
  @last_feedback = Time.now
126
113
  end
127
114
 
128
115
  def feedback_sandbox_connection(msg)
129
- return if @last_sandbox_feedback and (Time.now - @last_sandbox_feedback < FEEDBACK_TIME)
116
+ return if Time.now - @last_sandbox_feedback < FEEDBACK_TIME
130
117
  info "Get feedback sandbox information"
131
118
  conn = pool.feedback_sandbox_connection(msg)
132
119
  @last_sandbox_feedback = Time.now
@@ -150,8 +137,6 @@ module Suj
150
137
  conn.deliver(msg)
151
138
  end
152
139
 
153
-
154
-
155
140
  def redis_url
156
141
  @redis_url ||= "redis://#{Suj::Pusher.config.redis_host}:#{Suj::Pusher.config.redis_port}/#{Suj::Pusher.config.redis_db}"
157
142
  end
@@ -161,14 +146,41 @@ module Suj
161
146
  end
162
147
 
163
148
  def redis
164
- @redis ||= EM::Hiredis.connect(redis_url)
165
- end
149
+ return @redis if ! @redis.nil?
166
150
 
167
- def get_message
168
- @redis_connection ||= EM::Hiredis.connect(redis_url)
169
- @redis_connection.rpop "#{redis_namespace}:#{MSG_QUEUE}"
151
+ @redis = EM::Hiredis.connect(redis_url)
152
+
153
+ @redis.on(:connected) { info "REDIS - Connected to Redis server #{redis_url}" }
154
+
155
+ @redis.on(:closed) do
156
+ info "REDIS - Closed connection to Redis server"
157
+ @redis = nil
158
+ end
159
+
160
+ @redis.on(:failed) do
161
+ info "REDIS - redis connection FAILED"
162
+ @redis = nil
163
+ end
164
+
165
+ @redis.on(:reconnected) { info "REDIS - Reconnected to Redis server" }
166
+
167
+ @redis.on(:disconnected) do
168
+ info "REDIS - Disconnected from Redis server"
169
+ @redis = nil
170
+ end
171
+
172
+ @redis.on(:reconnect_failed) do
173
+ info "REDIS - Reconnection attempt to Redis server FAILED"
174
+ @redis = nil
175
+ end
176
+
177
+ return @redis
170
178
  end
171
179
 
180
+ # def get_message
181
+ # @redis_connection ||= EM::Hiredis.connect(redis_url)
182
+ # end
183
+
172
184
  def pool
173
185
  @pool ||= Suj::Pusher::ConnectionPool.new(self)
174
186
  end
@@ -1,5 +1,5 @@
1
1
  module Suj
2
2
  module Pusher
3
- VERSION = '0.2.0'
3
+ VERSION = '0.2.3'
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: suj-pusher
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-10-31 00:00:00.000000000 Z
12
+ date: 2013-12-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: em-http-request
16
- requirement: &16244860 !ruby/object:Gem::Requirement
16
+ requirement: &10281040 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *16244860
24
+ version_requirements: *10281040
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: em-hiredis
27
- requirement: &16244440 !ruby/object:Gem::Requirement
27
+ requirement: &10280620 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '0'
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *16244440
35
+ version_requirements: *10280620
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: multi_json
38
- requirement: &16244020 !ruby/object:Gem::Requirement
38
+ requirement: &10280020 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '0'
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *16244020
46
+ version_requirements: *10280020
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: daemon-spawn
49
- requirement: &16243600 !ruby/object:Gem::Requirement
49
+ requirement: &10279380 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,7 +54,18 @@ dependencies:
54
54
  version: '0'
55
55
  type: :runtime
56
56
  prerelease: false
57
- version_requirements: *16243600
57
+ version_requirements: *10279380
58
+ - !ruby/object:Gem::Dependency
59
+ name: iobuffer
60
+ requirement: &10295080 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :runtime
67
+ prerelease: false
68
+ version_requirements: *10295080
58
69
  description: Stand alone push notification server for APN, GCM, WNS and MPNS.
59
70
  email:
60
71
  - rd@skillupjapan.co.jp, n.bersano@skillupchile.cl