suj-pusher 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -62,13 +62,14 @@ Once the pusher daemon is running and connected to your redis server you can pus
62
62
 
63
63
  Example JSON message:
64
64
 
65
- ```json
65
+ ```
66
66
  {
67
67
  'apn_ids': ["xxxxx"],
68
68
  'gcm_ids': ["xxxxx", "yyyyyy"],
69
69
  'development': true,
70
70
  'cert': "cert string",
71
71
  'api_key': "secret key",
72
+ 'time_to_live': 0,
72
73
  'data': {
73
74
  'aps': {
74
75
  'alert': "This is a message"
@@ -82,6 +83,7 @@ Example JSON message:
82
83
  - development: This can be true or false and indicates if the push notification is to be sent using the APN sandbox gateway (yes) or the APN production gateway (no). This option only affects push notifications to iOS devices and is assumed yes if not provided.
83
84
  - cert: This is a string representation of the certificate used to send push notifications via the APN network. Simply read the cert.pem file as string and plug it in this field.
84
85
  - api_key: This is the secret api_key used to send push notifications via the GCM network. This is the key you get from the Google API console.
86
+ - time_to_live: Time in seconds the message would be stored on the cloud in case the destination device is not available at the moment. The default value is zero that means the message is discarded if the destination is not reachable at the moment the notification is sent. Note that even if you set this value larger than zero there are limitations that may prevent the message from arriving. For example Google allows to store up to 4 sync messages or 100 payload messages for up to time_to_live messages or max 4 weeks while Apple only stores the last message up to to time_to_live seconds.
85
87
  - data: This is a custom hash that is sent as push notification to the devices. For GCM this hash may contain anything you want as long as its size do not exceed 4096. For APN this data hash MUST contain an *aps* hash that follows Apple push notification format.
86
88
 
87
89
  #### Apple *aps* hash
@@ -41,11 +41,21 @@ module Suj
41
41
  end
42
42
 
43
43
  def deliver(data)
44
- @notification = Suj::Pusher::ApnNotification.new(data)
45
- if ! disconnected?
46
- info "APN delivering data"
47
- send_data(@notification.data)
48
- @notification = nil
44
+ begin
45
+ @notification = Suj::Pusher::ApnNotification.new(data)
46
+ if ! disconnected?
47
+ info "APN delivering data"
48
+ send_data(@notification.data)
49
+ @notification = nil
50
+ info "APN delivered data"
51
+ else
52
+ info "APN connection unavailable"
53
+ end
54
+ rescue Suj::Pusher::ApnNotification::PayloadTooLarge => e
55
+ error "APN notification payload too large."
56
+ debug @notification.data.inspect
57
+ rescue => ex
58
+ error "APN notification error : #{ex}"
49
59
  end
50
60
  end
51
61
 
@@ -67,9 +77,10 @@ module Suj
67
77
  info "APN Connection established..."
68
78
  @disconnected = false
69
79
  if ! @notification.nil?
70
- info "APN delivering data"
80
+ info "EST - APN delivering data"
71
81
  send_data(@notification.data)
72
82
  @notification = nil
83
+ info "EST - APN delivered data"
73
84
  end
74
85
  end
75
86
 
@@ -77,7 +88,7 @@ module Suj
77
88
  info "APN Connection closed..."
78
89
  @disconnected = true
79
90
  FileUtils.rm_f(@cert_file)
80
- @pool.remove_connection(@cert)
91
+ @pool.remove_connection(@cert_key)
81
92
  end
82
93
  end
83
94
  end
@@ -0,0 +1,50 @@
1
+ require "eventmachine"
2
+
3
+ require "base64"
4
+ module Suj
5
+ module Pusher
6
+ class APNFeedbackConnection < EM::Connection
7
+ include Suj::Pusher::Logger
8
+
9
+ def initialize(options = {})
10
+ super
11
+ @disconnected = true
12
+ @options = options
13
+ @cert_key = Digest::SHA1.hexdigest(@options[:cert])
14
+ @cert_file = File.join(Suj::Pusher.config.certs_path, @cert_key)
15
+ File.open(@cert_file, "w") do |f|
16
+ f.write @options[:cert]
17
+ end
18
+ @ssl_options = {
19
+ private_key_file: @cert_file,
20
+ cert_chain_file: @cert_file,
21
+ verify_peer: false
22
+ }
23
+ end
24
+
25
+ def disconnected?
26
+ @disconnected
27
+ end
28
+
29
+ def post_init
30
+ info "APN Feedback Connection init "
31
+ start_tls(@ssl_options)
32
+ end
33
+
34
+ def receive_data(data)
35
+ timestamp, size, token = data.unpack("QnN")
36
+ info "APN Feedback invalid token #{token}"
37
+ end
38
+
39
+ def connection_completed
40
+ info "APN Feedback Connection established..."
41
+ @disconnected = false
42
+ end
43
+
44
+ def unbind
45
+ info "APN Feedback Connection closed..."
46
+ @disconnected = true
47
+ end
48
+ end
49
+ end
50
+ end
@@ -9,6 +9,7 @@ module Suj
9
9
 
10
10
  def initialize(options = {})
11
11
  @token = options[:token]
12
+ @ttl = options[:time_to_live] || 0
12
13
  @options = options
13
14
  raise InvalidToken if @token.nil? || (@token.length != 64)
14
15
  raise PayloadTooLarge if data.size > MAX_SIZE
@@ -24,15 +25,22 @@ module Suj
24
25
 
25
26
  private
26
27
 
28
+ def get_expiry
29
+ if @ttl.to_i == 0
30
+ return 0
31
+ else
32
+ return Time.now.to_i + @ttl.to_i
33
+ end
34
+ end
35
+
27
36
  def encode_data
28
37
  identifier = 0
29
- expiry = 0
38
+ expiry = get_expiry
30
39
  size = [payload].pack("a*").size
31
40
  data_array = [1, identifier, expiry, 32, @token, size, payload]
32
41
  info("PAYLOAD: #{data_array}")
33
42
  data_array.pack("cNNnH*na*")
34
43
  end
35
-
36
44
  end
37
45
  end
38
46
  end
@@ -9,7 +9,10 @@ module Suj
9
9
 
10
10
  APN_SANDBOX = "gateway.sandbox.push.apple.com"
11
11
  APN_GATEWAY = "gateway.push.apple.com"
12
+ FEEDBACK_SANDBOX = "feedback.sandbox.push.apple.com"
13
+ FEEDBACK_GATEWAY = "feedback.push.apple.com"
12
14
  APN_PORT = 2195
15
+ FEEDBACK_PORT = 2196
13
16
 
14
17
  def initialize(daemon)
15
18
  @pool = {}
@@ -28,6 +31,16 @@ module Suj
28
31
  @pool[cert] ||= EM.connect(APN_SANDBOX, APN_PORT, APNConnection, self, options)
29
32
  end
30
33
 
34
+ def feedback_connection(options = {})
35
+ info "Feedback connection"
36
+ EM.connect(FEEDBACK_GATEWAY, FEEDBACK_PORT, APNFeedbackConnection, options)
37
+ end
38
+
39
+ def feedback_sandbox_connection(options = {})
40
+ info "Feedback sandbox connection"
41
+ EM.connect(FEEDBACK_SANDBOX, FEEDBACK_PORT, APNFeedbackConnection, options)
42
+ end
43
+
31
44
  def gcm_connection(options = {})
32
45
  # All GCM connections are unique, even if they are to the same app.
33
46
  api_key = "#{options[:api_key]}#{rand * 100}"
@@ -37,7 +50,7 @@ module Suj
37
50
 
38
51
  def remove_connection(key)
39
52
  info "Removing connection #{key}"
40
- @pool.delete(key)
53
+ info "Connection not found" unless @pool.delete(key)
41
54
  end
42
55
 
43
56
  end
@@ -11,15 +11,24 @@ module Suj
11
11
  class Daemon
12
12
  include Suj::Pusher::Logger
13
13
 
14
+ FEEDBACK_TIME = 43200 # 12H
15
+
14
16
  def start
15
17
  info "Starting pusher daemon"
16
18
  EM.run do
17
19
  wait_msg do |msg|
18
20
  begin
21
+ info "RECEIVED MESSAGE"
19
22
  data = Hash.symbolize_keys(MultiJson.load(msg))
20
23
  send_notification(data)
24
+ info "SENT MESSAGE"
25
+ retrieve_feedback(data)
26
+ info "FINISHED FEEDBACK RETRIEVAL"
21
27
  rescue MultiJson::LoadError
22
28
  warn("Received invalid json data, discarding msg")
29
+ rescue => e
30
+ error("Error sending notification : #{e}")
31
+ error e.backtrace
23
32
  end
24
33
  end
25
34
  end
@@ -37,8 +46,28 @@ module Suj
37
46
  private
38
47
 
39
48
  def wait_msg
49
+ redis.on(:connected) { info "REDIS - Connected to Redis server" }
50
+ redis.on(:closed) { info "REDIS - Closed connection to Redis server" }
51
+ redis.on(:failed) { info "REDIS - redis connection FAILED" }
52
+ redis.on(:reconnected) { info "REDIS - Reconnected to Redis server" }
53
+ redis.on(:disconnected) { info "REDIS - Disconnected from Redis server" }
54
+ redis.on(:reconnect_failed) { info "REDIS - Reconnection attempt to Redis server FAILED" }
55
+ # EM.add_periodic_timer(30) { redis.publish Suj::Pusher::QUEUE, "ECHO" }
40
56
  redis.pubsub.subscribe(Suj::Pusher::QUEUE) do |msg|
41
- yield msg
57
+ if msg == "ECHO"
58
+ info "REDIS - ECHO Received"
59
+ elsif msg == "PUSH_MSG"
60
+ info "REDIS - PUSH_MSG Received"
61
+ get_message.callback do |message|
62
+ if message
63
+ yield message
64
+ else
65
+ info "REDIS - PUSH_MSG Queue was empty"
66
+ end
67
+ end
68
+ else
69
+ yield msg
70
+ end
42
71
  end
43
72
  end
44
73
 
@@ -56,6 +85,16 @@ module Suj
56
85
  end
57
86
  end
58
87
 
88
+ def retrieve_feedback(msg)
89
+ if msg.has_key?(:cert)
90
+ if msg.has_key?(:development) && msg[:development]
91
+ feedback_sandbox_connection(msg)
92
+ else
93
+ feedback_connection(msg)
94
+ end
95
+ end
96
+ end
97
+
59
98
  def send_apn_notification(msg)
60
99
  info "Sending APN notification via connection #{Digest::SHA1.hexdigest(msg[:cert])}"
61
100
  conn = pool.apn_connection(msg)
@@ -72,6 +111,20 @@ module Suj
72
111
  end
73
112
  end
74
113
 
114
+ def feedback_connection(msg)
115
+ return if @last_feedback and (Time.now - @last_feedback < FEEDBACK_TIME)
116
+ info "Get feedback information"
117
+ conn = pool.feedback_connection(msg)
118
+ @last_feedback = Time.now
119
+ end
120
+
121
+ def feedback_sandbox_connection(msg)
122
+ return if @last_sandbox_feedback and (Time.now - @last_sandbox_feedback < FEEDBACK_TIME)
123
+ info "Get feedback sandbox information"
124
+ conn = pool.feedback_sandbox_connection(msg)
125
+ @last_sandbox_feedback = Time.now
126
+ end
127
+
75
128
  def send_gcm_notification(msg)
76
129
  info "Sending GCM notification via connection #{msg[:api_key]}"
77
130
  conn = pool.gcm_connection(msg)
@@ -79,7 +132,12 @@ module Suj
79
132
  end
80
133
 
81
134
  def redis
82
- @redis || EM::Hiredis.connect(Suj::Pusher.config.redis)
135
+ @redis ||= EM::Hiredis.connect(Suj::Pusher.config.redis)
136
+ end
137
+
138
+ def get_message
139
+ @redis_connection ||= EM::Hiredis.connect(Suj::Pusher.config.redis)
140
+ @redis_connection.rpop MSG_QUEUE
83
141
  end
84
142
 
85
143
  def pool
@@ -23,10 +23,10 @@ module Suj
23
23
 
24
24
  body = MultiJson.dump({
25
25
  registration_ids: msg[:gcm_ids],
26
+ time_to_live: msg[:time_to_live] || 0,
26
27
  data: msg[:data] || {}
27
28
  })
28
29
 
29
-
30
30
  http = EventMachine::HttpRequest.new(GATEWAY).post( head: @headers, body: body )
31
31
 
32
32
  http.errback do
@@ -1,5 +1,5 @@
1
1
  module Suj
2
2
  module Pusher
3
- VERSION = '0.0.1'
3
+ VERSION = '0.1.0'
4
4
  end
5
5
  end
data/lib/suj/pusher.rb CHANGED
@@ -3,6 +3,7 @@ require 'suj/pusher/version'
3
3
  require 'suj/pusher/configuration'
4
4
  require 'suj/pusher/logger'
5
5
  require 'suj/pusher/connection_pool'
6
+ require 'suj/pusher/apn_feedback_connection'
6
7
  require 'suj/pusher/apn_connection'
7
8
  require 'suj/pusher/gcm_connection'
8
9
  require 'suj/pusher/apn_notification'
@@ -13,7 +14,7 @@ require 'logger'
13
14
  module Suj
14
15
  module Pusher
15
16
 
16
- QUEUE = "suj_pusher_queue"
17
-
17
+ QUEUE = "suj_pusher_queue"
18
+ MSG_QUEUE = "suj_pusher_msgs"
18
19
  end
19
20
  end
metadata CHANGED
@@ -1,19 +1,20 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: suj-pusher
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
8
8
  - Horacio Sanson
9
+ - Fernando Wong
9
10
  autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 2013-07-26 00:00:00.000000000 Z
13
+ date: 2013-10-10 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
16
  name: em-http-request
16
- requirement: &19868920 !ruby/object:Gem::Requirement
17
+ requirement: !ruby/object:Gem::Requirement
17
18
  none: false
18
19
  requirements:
19
20
  - - ! '>='
@@ -21,10 +22,15 @@ dependencies:
21
22
  version: '0'
22
23
  type: :runtime
23
24
  prerelease: false
24
- version_requirements: *19868920
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ! '>='
29
+ - !ruby/object:Gem::Version
30
+ version: '0'
25
31
  - !ruby/object:Gem::Dependency
26
32
  name: em-hiredis
27
- requirement: &19868320 !ruby/object:Gem::Requirement
33
+ requirement: !ruby/object:Gem::Requirement
28
34
  none: false
29
35
  requirements:
30
36
  - - ! '>='
@@ -32,10 +38,15 @@ dependencies:
32
38
  version: '0'
33
39
  type: :runtime
34
40
  prerelease: false
35
- version_requirements: *19868320
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
36
47
  - !ruby/object:Gem::Dependency
37
48
  name: multi_json
38
- requirement: &19867580 !ruby/object:Gem::Requirement
49
+ requirement: !ruby/object:Gem::Requirement
39
50
  none: false
40
51
  requirements:
41
52
  - - ! '>='
@@ -43,10 +54,15 @@ dependencies:
43
54
  version: '0'
44
55
  type: :runtime
45
56
  prerelease: false
46
- version_requirements: *19867580
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
47
63
  - !ruby/object:Gem::Dependency
48
64
  name: daemon-spawn
49
- requirement: &19866460 !ruby/object:Gem::Requirement
65
+ requirement: !ruby/object:Gem::Requirement
50
66
  none: false
51
67
  requirements:
52
68
  - - ! '>='
@@ -54,7 +70,12 @@ dependencies:
54
70
  version: '0'
55
71
  type: :runtime
56
72
  prerelease: false
57
- version_requirements: *19866460
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
58
79
  description: Stand alone push notification server for APN and GCM.
59
80
  email:
60
81
  - rd@skillupjapan.co.jp
@@ -67,6 +88,7 @@ files:
67
88
  - README.md
68
89
  - lib/suj/pusher.rb
69
90
  - lib/suj/pusher/apn_connection.rb
91
+ - lib/suj/pusher/apn_feedback_connection.rb
70
92
  - lib/suj/pusher/apn_notification.rb
71
93
  - lib/suj/pusher/configuration.rb
72
94
  - lib/suj/pusher/connection_pool.rb
@@ -96,7 +118,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
96
118
  version: '0'
97
119
  requirements: []
98
120
  rubyforge_project:
99
- rubygems_version: 1.8.12
121
+ rubygems_version: 1.8.23
100
122
  signing_key:
101
123
  specification_version: 3
102
124
  summary: Stand alone push notification server.