rapns 2.0.5 → 3.0.0.beta.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.
Files changed (82) hide show
  1. data/lib/generators/rapns_generator.rb +1 -0
  2. data/lib/generators/templates/add_gcm.rb +86 -0
  3. data/lib/generators/templates/create_rapns_notifications.rb +1 -1
  4. data/lib/rapns/apns/app.rb +8 -0
  5. data/lib/rapns/apns/binary_notification_validator.rb +12 -0
  6. data/lib/rapns/apns/device_token_format_validator.rb +12 -0
  7. data/lib/rapns/apns/feedback.rb +14 -0
  8. data/lib/rapns/apns/notification.rb +84 -0
  9. data/lib/rapns/app.rb +5 -6
  10. data/lib/rapns/{config.rb → configuration.rb} +5 -5
  11. data/lib/rapns/daemon/apns/app_runner.rb +36 -0
  12. data/lib/rapns/daemon/apns/connection.rb +113 -0
  13. data/lib/rapns/daemon/apns/delivery.rb +63 -0
  14. data/lib/rapns/daemon/apns/delivery_handler.rb +21 -0
  15. data/lib/rapns/daemon/apns/disconnection_error.rb +20 -0
  16. data/lib/rapns/daemon/apns/feedback_receiver.rb +74 -0
  17. data/lib/rapns/daemon/app_runner.rb +76 -77
  18. data/lib/rapns/daemon/database_reconnectable.rb +3 -3
  19. data/lib/rapns/daemon/delivery.rb +43 -0
  20. data/lib/rapns/daemon/delivery_error.rb +6 -2
  21. data/lib/rapns/daemon/delivery_handler.rb +13 -79
  22. data/lib/rapns/daemon/delivery_queue_18.rb +2 -2
  23. data/lib/rapns/daemon/delivery_queue_19.rb +3 -3
  24. data/lib/rapns/daemon/feeder.rb +5 -5
  25. data/lib/rapns/daemon/gcm/app_runner.rb +13 -0
  26. data/lib/rapns/daemon/gcm/delivery.rb +206 -0
  27. data/lib/rapns/daemon/gcm/delivery_handler.rb +20 -0
  28. data/lib/rapns/daemon.rb +31 -20
  29. data/lib/rapns/gcm/app.rb +7 -0
  30. data/lib/rapns/gcm/expiry_collapse_key_mutual_inclusion_validator.rb +11 -0
  31. data/lib/rapns/gcm/notification.rb +31 -0
  32. data/lib/rapns/gcm/payload_size_validator.rb +13 -0
  33. data/lib/rapns/multi_json_helper.rb +16 -0
  34. data/lib/rapns/notification.rb +28 -95
  35. data/lib/rapns/version.rb +1 -1
  36. data/lib/rapns.rb +14 -4
  37. data/lib/tasks/cane.rake +19 -0
  38. data/lib/tasks/test.rake +34 -0
  39. data/spec/acceptance/gcm_upgrade_spec.rb +34 -0
  40. data/spec/acceptance_spec_helper.rb +85 -0
  41. data/spec/support/simplecov_helper.rb +13 -0
  42. data/spec/support/simplecov_quality_formatter.rb +8 -0
  43. data/spec/unit/apns/app_spec.rb +15 -0
  44. data/spec/unit/apns/feedback_spec.rb +12 -0
  45. data/spec/{rapns → unit/apns}/notification_spec.rb +44 -72
  46. data/spec/unit/app_spec.rb +18 -0
  47. data/spec/unit/daemon/apns/app_runner_spec.rb +37 -0
  48. data/spec/{rapns/daemon → unit/daemon/apns}/connection_spec.rb +9 -9
  49. data/spec/unit/daemon/apns/delivery_handler_spec.rb +48 -0
  50. data/spec/unit/daemon/apns/delivery_spec.rb +154 -0
  51. data/spec/{rapns/daemon → unit/daemon/apns}/feedback_receiver_spec.rb +14 -14
  52. data/spec/unit/daemon/app_runner_shared.rb +66 -0
  53. data/spec/unit/daemon/app_runner_spec.rb +78 -0
  54. data/spec/{rapns → unit}/daemon/database_reconnectable_spec.rb +4 -5
  55. data/spec/{rapns → unit}/daemon/delivery_error_spec.rb +2 -2
  56. data/spec/unit/daemon/delivery_handler_shared.rb +19 -0
  57. data/spec/{rapns → unit}/daemon/delivery_queue_spec.rb +1 -1
  58. data/spec/{rapns → unit}/daemon/feeder_spec.rb +33 -33
  59. data/spec/unit/daemon/gcm/app_runner_spec.rb +15 -0
  60. data/spec/unit/daemon/gcm/delivery_handler_spec.rb +36 -0
  61. data/spec/unit/daemon/gcm/delivery_spec.rb +236 -0
  62. data/spec/{rapns → unit}/daemon/interruptible_sleep_spec.rb +1 -1
  63. data/spec/{rapns → unit}/daemon/logger_spec.rb +1 -1
  64. data/spec/{rapns → unit}/daemon_spec.rb +1 -1
  65. data/spec/unit/gcm/app_spec.rb +5 -0
  66. data/spec/unit/gcm/notification_spec.rb +55 -0
  67. data/spec/unit/notification_shared.rb +38 -0
  68. data/spec/unit/notification_spec.rb +6 -0
  69. data/spec/{rapns/app_spec.rb → unit_spec_helper.rb} +76 -16
  70. metadata +107 -45
  71. data/lib/rapns/binary_notification_validator.rb +0 -10
  72. data/lib/rapns/daemon/connection.rb +0 -114
  73. data/lib/rapns/daemon/delivery_handler_pool.rb +0 -18
  74. data/lib/rapns/daemon/disconnection_error.rb +0 -14
  75. data/lib/rapns/daemon/feedback_receiver.rb +0 -82
  76. data/lib/rapns/device_token_format_validator.rb +0 -10
  77. data/lib/rapns/feedback.rb +0 -12
  78. data/spec/rapns/daemon/app_runner_spec.rb +0 -193
  79. data/spec/rapns/daemon/delivery_handler_pool_spec.rb +0 -17
  80. data/spec/rapns/daemon/delivery_handler_spec.rb +0 -206
  81. data/spec/rapns/feedback_spec.rb +0 -12
  82. data/spec/spec_helper.rb +0 -78
@@ -15,6 +15,7 @@ class RapnsGenerator < Rails::Generators::Base
15
15
  add_rapns_migration('add_alert_is_json_to_rapns_notifications')
16
16
  add_rapns_migration('add_app_to_rapns')
17
17
  add_rapns_migration('create_rapns_apps')
18
+ add_rapns_migration('add_gcm')
18
19
  end
19
20
 
20
21
  protected
@@ -0,0 +1,86 @@
1
+ class AddGcm < ActiveRecord::Migration
2
+ module Rapns
3
+ class App < ActiveRecord::Base
4
+ self.table_name = 'rapns_apps'
5
+ end
6
+
7
+ class Notification < ActiveRecord::Base
8
+ belongs_to :app
9
+ self.table_name = 'rapns_notifications'
10
+ end
11
+ end
12
+
13
+ def self.up
14
+ add_column :rapns_notifications, :type, :string, :null => true
15
+ add_column :rapns_apps, :type, :string, :null => true
16
+
17
+ AddGcm::Rapns::Notification.update_all :type => 'Rapns::Apns::Notification'
18
+ AddGcm::Rapns::App.update_all :type => 'Rapns::Apns::App'
19
+
20
+ change_column_null :rapns_notifications, :type, false
21
+ change_column_null :rapns_apps, :type, false
22
+ change_column_null :rapns_notifications, :device_token, true
23
+ change_column_null :rapns_notifications, :expiry, true
24
+ change_column_null :rapns_apps, :environment, true
25
+ change_column_null :rapns_apps, :certificate, true
26
+
27
+ change_column :rapns_notifications, :error_description, :text
28
+
29
+ rename_column :rapns_notifications, :attributes_for_device, :data
30
+ rename_column :rapns_apps, :key, :name
31
+
32
+ add_column :rapns_apps, :auth_key, :string, :null => true
33
+
34
+ add_column :rapns_notifications, :collapse_key, :string, :null => true
35
+ add_column :rapns_notifications, :delay_while_idle, :boolean, :null => false, :default => false
36
+ add_column :rapns_notifications, :registration_ids, :text, :null => true
37
+ add_column :rapns_notifications, :app_id, :integer, :null => true
38
+ add_column :rapns_notifications, :retries, :integer, :null => true, :default => 0
39
+
40
+ Rapns::Notification.reset_column_information
41
+ Rapns::App.reset_column_information
42
+
43
+ Rapns::App.all.each do |app|
44
+ Rapns::Notification.update_all(['app_id = ?', app.id], ['app = ?', app.name])
45
+ end
46
+
47
+ change_column_null :rapns_notifications, :app_id, false
48
+ remove_column :rapns_notifications, :app
49
+ end
50
+
51
+ def self.down
52
+ AddGcm::Rapns::Notification.where(:type => 'Rapns::Gcm::Notification').delete_all
53
+
54
+ remove_column :rapns_notifications, :type
55
+ remove_column :rapns_apps, :type
56
+
57
+ change_column_null :rapns_notifications, :device_token, false
58
+ change_column_null :rapns_notifications, :expiry, false
59
+ change_column_null :rapns_apps, :environment, false
60
+ change_column_null :rapns_apps, :certificate, false
61
+
62
+ change_column :rapns_notifications, :error_description, :string
63
+
64
+ rename_column :rapns_notifications, :data, :attributes_for_device
65
+ rename_column :rapns_apps, :name, :key
66
+
67
+ remove_column :rapns_apps, :auth_key
68
+
69
+ remove_column :rapns_notifications, :collapse_key
70
+ remove_column :rapns_notifications, :delay_while_idle
71
+ remove_column :rapns_notifications, :registration_ids
72
+ remove_column :rapns_notifications, :retries
73
+
74
+ add_column :rapns_notifications, :app, :string, :null => true
75
+
76
+ Rapns::Notification.reset_column_information
77
+ Rapns::App.reset_column_information
78
+
79
+ Rapns::App.all.each do |app|
80
+ Rapns::Notification.update_all(['app = ?', app.key], ['app_id = ?', app.id])
81
+ end
82
+
83
+ change_column_null :rapns_notifications, :key, false
84
+ remove_column :rapns_notifications, :app_id
85
+ end
86
+ end
@@ -17,7 +17,7 @@ class CreateRapnsNotifications < ActiveRecord::Migration
17
17
  t.timestamps
18
18
  end
19
19
 
20
- add_index :rapns_notifications, [:delivered, :failed, :deliver_after], :name => "index_rapns_notifications_on_delivered_failed_deliver_after"
20
+ add_index :rapns_notifications, [:delivered, :failed, :deliver_after], :name => "index_rapns_notifications_multi"
21
21
  end
22
22
 
23
23
  def self.down
@@ -0,0 +1,8 @@
1
+ module Rapns
2
+ module Apns
3
+ class App < Rapns::App
4
+ validates :environment, :presence => true, :inclusion => { :in => %w(development production) }
5
+ validates :certificate, :presence => true
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,12 @@
1
+ module Rapns
2
+ module Apns
3
+ class BinaryNotificationValidator < ActiveModel::Validator
4
+
5
+ def validate(record)
6
+ if record.payload_size > 256
7
+ record.errors[:base] << "APN notification cannot be larger than 256 bytes. Try condensing your alert and device attributes."
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module Rapns
2
+ module Apns
3
+ class DeviceTokenFormatValidator < ActiveModel::Validator
4
+
5
+ def validate(record)
6
+ if record.device_token !~ /^[a-z0-9]{64}$/
7
+ record.errors[:device_token] << "is invalid"
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ module Rapns
2
+ module Apns
3
+ class Feedback < ActiveRecord::Base
4
+ self.table_name = 'rapns_feedback'
5
+
6
+ attr_accessible :device_token, :failed_at, :app
7
+
8
+ validates :device_token, :presence => true
9
+ validates :failed_at, :presence => true
10
+
11
+ validates_with Rapns::Apns::DeviceTokenFormatValidator
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,84 @@
1
+ module Rapns
2
+ module Apns
3
+ class Notification < Rapns::Notification
4
+ class MultipleAppAssignmentError < StandardError; end
5
+
6
+ validates :device_token, :presence => true
7
+ validates :badge, :numericality => true, :allow_nil => true
8
+
9
+ validates_with Rapns::Apns::DeviceTokenFormatValidator
10
+ validates_with Rapns::Apns::BinaryNotificationValidator
11
+
12
+ alias_method :attributes_for_device=, :data=
13
+ alias_method :attributes_for_device, :data
14
+
15
+ def device_token=(token)
16
+ write_attribute(:device_token, token.delete(" <>")) if !token.nil?
17
+ end
18
+
19
+ def alert=(alert)
20
+ if alert.is_a?(Hash)
21
+ write_attribute(:alert, multi_json_dump(alert))
22
+ self.alert_is_json = true if has_attribute?(:alert_is_json)
23
+ else
24
+ write_attribute(:alert, alert)
25
+ self.alert_is_json = false if has_attribute?(:alert_is_json)
26
+ end
27
+ end
28
+
29
+ def alert
30
+ string_or_json = read_attribute(:alert)
31
+
32
+ if has_attribute?(:alert_is_json)
33
+ if alert_is_json?
34
+ multi_json_load(string_or_json)
35
+ else
36
+ string_or_json
37
+ end
38
+ else
39
+ multi_json_load(string_or_json) rescue string_or_json
40
+ end
41
+ end
42
+
43
+ MDM_KEY = '__rapns_mdm__'
44
+ def mdm=(magic)
45
+ self.attributes_for_device = { MDM_KEY => magic }
46
+ end
47
+
48
+ CONTENT_AVAILABLE_KEY = '__rapns_content_available__'
49
+ def content_available=(bool)
50
+ return unless bool
51
+ self.attributes_for_device = { CONTENT_AVAILABLE_KEY => true }
52
+ end
53
+
54
+ def as_json
55
+ json = ActiveSupport::OrderedHash.new
56
+
57
+ if attributes_for_device && attributes_for_device.key?(MDM_KEY)
58
+ json['mdm'] = attributes_for_device[MDM_KEY]
59
+ else
60
+ json['aps'] = ActiveSupport::OrderedHash.new
61
+ json['aps']['alert'] = alert if alert
62
+ json['aps']['badge'] = badge if badge
63
+ json['aps']['sound'] = sound if sound
64
+
65
+ if attributes_for_device && attributes_for_device[CONTENT_AVAILABLE_KEY]
66
+ json['aps']['content-available'] = 1
67
+ end
68
+
69
+ if attributes_for_device
70
+ non_aps_attributes = attributes_for_device.reject { |k, v| k == CONTENT_AVAILABLE_KEY }
71
+ non_aps_attributes.each { |k, v| json[k.to_s] = v.to_s }
72
+ end
73
+ end
74
+
75
+ json
76
+ end
77
+
78
+ def to_binary(options = {})
79
+ id_for_pack = options[:for_validation] ? 0 : id
80
+ [1, id_for_pack, expiry, 0, 32, device_token, payload_size, payload].pack("cNNccH*na*")
81
+ end
82
+ end
83
+ end
84
+ end
data/lib/rapns/app.rb CHANGED
@@ -2,11 +2,11 @@ module Rapns
2
2
  class App < ActiveRecord::Base
3
3
  self.table_name = 'rapns_apps'
4
4
 
5
- attr_accessible :key, :environment, :certificate, :password, :connections
5
+ attr_accessible :name, :environment, :certificate, :password, :connections, :auth_key
6
6
 
7
- validates :key, :presence => true, :uniqueness => true
8
- validates :environment, :presence => true, :inclusion => { :in => %w(development production) }
9
- validates :certificate, :presence => true
7
+ has_many :notifications
8
+
9
+ validates :name, :presence => true, :uniqueness => { :scope => [:type, :environment] }
10
10
  validates_numericality_of :connections, :greater_than => 0, :only_integer => true
11
11
 
12
12
  validate :certificate_has_matching_private_key
@@ -20,11 +20,10 @@ module Rapns
20
20
  pkey = OpenSSL::PKey::RSA.new certificate rescue nil
21
21
  result = !x509.nil? && !pkey.nil?
22
22
  unless result
23
- errors.add :certificate, "Certificate value must contain a certificate and a private key"
23
+ errors.add :certificate, 'Certificate value must contain a certificate and a private key.'
24
24
  end
25
25
  end
26
26
  result
27
27
  end
28
28
  end
29
29
  end
30
-
@@ -1,11 +1,11 @@
1
1
  module Rapns
2
2
 
3
- # A globally accessible instance of Rapns::Config
3
+ # A globally accessible instance of Rapns::Configuration
4
4
  def self.configuration
5
- @configuration ||= Rapns::Config.new
5
+ @configuration ||= Rapns::Configuration.new
6
6
  end
7
7
 
8
- # Call the given block yielding to it the global Rapns::Config instance for setting
8
+ # Call the given block yielding to it the global Rapns::Configuration instance for setting
9
9
  # configuration values / callbacks.
10
10
  #
11
11
  # Typically this would be used in your Rails application's config/initializers/rapns.rb file
@@ -14,7 +14,7 @@ module Rapns
14
14
  end
15
15
 
16
16
  # A class to hold Rapns configuration settings and callbacks.
17
- class Config < Struct.new(:foreground, :push_poll, :feedback_poll, :airbrake_notify, :check_for_errors, :pid_file, :batch_size)
17
+ class Configuration < Struct.new(:foreground, :push_poll, :feedback_poll, :airbrake_notify, :check_for_errors, :pid_file, :batch_size)
18
18
 
19
19
  attr_accessor :feedback_callback
20
20
 
@@ -52,4 +52,4 @@ module Rapns
52
52
  self.feedback_callback = block
53
53
  end
54
54
  end
55
- end
55
+ end
@@ -0,0 +1,36 @@
1
+ module Rapns
2
+ module Daemon
3
+ module Apns
4
+ class AppRunner < Rapns::Daemon::AppRunner
5
+ ENVIRONMENTS = {
6
+ :production => {
7
+ :push => ['gateway.push.apple.com', 2195],
8
+ :feedback => ['feedback.push.apple.com', 2196]
9
+ },
10
+ :development => {
11
+ :push => ['gateway.sandbox.push.apple.com', 2195],
12
+ :feedback => ['feedback.sandbox.push.apple.com', 2196]
13
+ }
14
+ }
15
+
16
+ protected
17
+
18
+ def started
19
+ poll = Rapns::Daemon.config[:feedback_poll]
20
+ host, port = ENVIRONMENTS[app.environment.to_sym][:feedback]
21
+ @feedback_receiver = FeedbackReceiver.new(app, host, port, poll)
22
+ @feedback_receiver.start
23
+ end
24
+
25
+ def stopped
26
+ @feedback_receiver.stop if @feedback_receiver
27
+ end
28
+
29
+ def new_delivery_handler
30
+ push_host, push_port = ENVIRONMENTS[app.environment.to_sym][:push]
31
+ DeliveryHandler.new(app, push_host, push_port)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,113 @@
1
+ module Rapns
2
+ module Daemon
3
+ module Apns
4
+ class ConnectionError < StandardError; end
5
+
6
+ class Connection
7
+ attr_accessor :last_write
8
+
9
+ def self.idle_period
10
+ 30.minutes
11
+ end
12
+
13
+ def initialize(name, host, port, certificate, password)
14
+ @name = name
15
+ @host = host
16
+ @port = port
17
+ @certificate = certificate
18
+ @password = password
19
+ written
20
+ end
21
+
22
+ def connect
23
+ @ssl_context = setup_ssl_context
24
+ @tcp_socket, @ssl_socket = connect_socket
25
+ end
26
+
27
+ def close
28
+ begin
29
+ @ssl_socket.close if @ssl_socket
30
+ @tcp_socket.close if @tcp_socket
31
+ rescue IOError
32
+ end
33
+ end
34
+
35
+ def read(num_bytes)
36
+ @ssl_socket.read(num_bytes)
37
+ end
38
+
39
+ def select(timeout)
40
+ IO.select([@ssl_socket], nil, nil, timeout)
41
+ end
42
+
43
+ def write(data)
44
+ reconnect_idle if idle_period_exceeded?
45
+
46
+ retry_count = 0
47
+
48
+ begin
49
+ write_data(data)
50
+ rescue Errno::EPIPE, Errno::ETIMEDOUT, OpenSSL::SSL::SSLError => e
51
+ retry_count += 1;
52
+
53
+ if retry_count == 1
54
+ Rapns::Daemon.logger.error("[#{@name}] Lost connection to #{@host}:#{@port} (#{e.class.name}), reconnecting...")
55
+ end
56
+
57
+ if retry_count <= 3
58
+ reconnect
59
+ sleep 1
60
+ retry
61
+ else
62
+ raise ConnectionError, "#{@name} tried #{retry_count-1} times to reconnect but failed (#{e.class.name})."
63
+ end
64
+ end
65
+ end
66
+
67
+ def reconnect
68
+ close
69
+ @tcp_socket, @ssl_socket = connect_socket
70
+ end
71
+
72
+ protected
73
+
74
+ def reconnect_idle
75
+ Rapns::Daemon.logger.info("[#{@name}] Idle period exceeded, reconnecting...")
76
+ reconnect
77
+ end
78
+
79
+ def idle_period_exceeded?
80
+ Time.now - last_write > self.class.idle_period
81
+ end
82
+
83
+ def write_data(data)
84
+ @ssl_socket.write(data)
85
+ @ssl_socket.flush
86
+ written
87
+ end
88
+
89
+ def written
90
+ self.last_write = Time.now
91
+ end
92
+
93
+ def setup_ssl_context
94
+ ssl_context = OpenSSL::SSL::SSLContext.new
95
+ ssl_context.key = OpenSSL::PKey::RSA.new(@certificate, @password)
96
+ ssl_context.cert = OpenSSL::X509::Certificate.new(@certificate)
97
+ ssl_context
98
+ end
99
+
100
+ def connect_socket
101
+ tcp_socket = TCPSocket.new(@host, @port)
102
+ tcp_socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, 1)
103
+ tcp_socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
104
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, @ssl_context)
105
+ ssl_socket.sync = true
106
+ ssl_socket.connect
107
+ Rapns::Daemon.logger.info("[#{@name}] Connected to #{@host}:#{@port}")
108
+ [tcp_socket, ssl_socket]
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,63 @@
1
+ module Rapns
2
+ module Daemon
3
+ module Apns
4
+ class Delivery < Rapns::Daemon::Delivery
5
+ SELECT_TIMEOUT = 0.2
6
+ ERROR_TUPLE_BYTES = 6
7
+ APN_ERRORS = {
8
+ 1 => "Processing error",
9
+ 2 => "Missing device token",
10
+ 3 => "Missing topic",
11
+ 4 => "Missing payload",
12
+ 5 => "Missing token size",
13
+ 6 => "Missing topic size",
14
+ 7 => "Missing payload size",
15
+ 8 => "Invalid token",
16
+ 255 => "None (unknown error)"
17
+ }
18
+
19
+ def initialize(app, conneciton, notification)
20
+ @app = app
21
+ @connection = conneciton
22
+ @notification = notification
23
+ end
24
+
25
+ def perform
26
+ begin
27
+ @connection.write(@notification.to_binary)
28
+ check_for_error if Rapns::Daemon.config.check_for_errors
29
+ mark_delivered
30
+ Rapns::Daemon.logger.info("[#{@app.name}] #{@notification.id} sent to #{@notification.device_token}")
31
+ rescue Rapns::DeliveryError, Rapns::Apns::DisconnectionError => error
32
+ mark_failed(error.code, error.description)
33
+ raise
34
+ end
35
+ end
36
+
37
+ protected
38
+
39
+ def check_for_error
40
+ if @connection.select(SELECT_TIMEOUT)
41
+ error = nil
42
+
43
+ if tuple = @connection.read(ERROR_TUPLE_BYTES)
44
+ cmd, code, notification_id = tuple.unpack("ccN")
45
+
46
+ description = APN_ERRORS[code.to_i] || "Unknown error. Possible rapns bug?"
47
+ error = Rapns::DeliveryError.new(code, notification_id, description)
48
+ else
49
+ error = Rapns::Apns::DisconnectionError.new
50
+ end
51
+
52
+ begin
53
+ Rapns::Daemon.logger.error("[#{@app.name}] Error received, reconnecting...")
54
+ @connection.reconnect
55
+ ensure
56
+ raise error if error
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,21 @@
1
+ module Rapns
2
+ module Daemon
3
+ module Apns
4
+ class DeliveryHandler < Rapns::Daemon::DeliveryHandler
5
+ def initialize(app, host, port)
6
+ @app = app
7
+ @connection = Connection.new(@app.name, host, port, @app.certificate, @app.password)
8
+ @connection.connect
9
+ end
10
+
11
+ def deliver(notification)
12
+ Rapns::Daemon::Apns::Delivery.perform(@app, @connection, notification)
13
+ end
14
+
15
+ def stopped
16
+ @connection.close
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ module Rapns
2
+ module Apns
3
+ class DisconnectionError < StandardError
4
+ attr_reader :code, :description
5
+
6
+ def initialize
7
+ @code = nil
8
+ @description = "APNs disconnected without returning an error."
9
+ end
10
+
11
+ def to_s
12
+ message
13
+ end
14
+
15
+ def message
16
+ "The APNs disconnected without returning an error. This may indicate you are using an invalid certificate for the host."
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,74 @@
1
+ module Rapns
2
+ module Daemon
3
+ module Apns
4
+ class FeedbackReceiver
5
+ include InterruptibleSleep
6
+ include DatabaseReconnectable
7
+
8
+ FEEDBACK_TUPLE_BYTES = 38
9
+
10
+ def initialize(app, host, port, poll)
11
+ @app = app
12
+ @host = host
13
+ @port = port
14
+ @poll = poll
15
+ @certificate = app.certificate
16
+ @password = app.password
17
+ end
18
+
19
+ def start
20
+ @thread = Thread.new do
21
+ loop do
22
+ break if @stop
23
+ check_for_feedback
24
+ interruptible_sleep @poll
25
+ end
26
+ end
27
+ end
28
+
29
+ def stop
30
+ @stop = true
31
+ interrupt_sleep
32
+ @thread.join if @thread
33
+ end
34
+
35
+ def check_for_feedback
36
+ connection = nil
37
+ begin
38
+ connection = Connection.new("FeedbackReceiver:#{@app.name}", @host, @port, @certificate, @password)
39
+ connection.connect
40
+
41
+ while tuple = connection.read(FEEDBACK_TUPLE_BYTES)
42
+ timestamp, device_token = parse_tuple(tuple)
43
+ create_feedback(timestamp, device_token)
44
+ end
45
+ rescue StandardError => e
46
+ Rapns::Daemon.logger.error(e)
47
+ ensure
48
+ connection.close if connection
49
+ end
50
+ end
51
+
52
+ protected
53
+
54
+ def parse_tuple(tuple)
55
+ failed_at, _, device_token = tuple.unpack("N1n1H*")
56
+ [Time.at(failed_at).utc, device_token]
57
+ end
58
+
59
+ def create_feedback(failed_at, device_token)
60
+ formatted_failed_at = failed_at.strftime("%Y-%m-%d %H:%M:%S UTC")
61
+ with_database_reconnect_and_retry do
62
+ Rapns::Daemon.logger.info("[FeedbackReceiver:#{@app.name}] Delivery failed at #{formatted_failed_at} for #{device_token}")
63
+ feedback = Rapns::Apns::Feedback.create!(:failed_at => failed_at, :device_token => device_token, :app => @app)
64
+ begin
65
+ Rapns.configuration.feedback_callback.call(feedback) if Rapns.configuration.feedback_callback
66
+ rescue StandardError => e
67
+ Rapns::Daemon.logger.error(e)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end