rapns 2.0.5 → 3.0.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
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