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.
- data/lib/generators/rapns_generator.rb +1 -0
- data/lib/generators/templates/add_gcm.rb +86 -0
- data/lib/generators/templates/create_rapns_notifications.rb +1 -1
- data/lib/rapns/apns/app.rb +8 -0
- data/lib/rapns/apns/binary_notification_validator.rb +12 -0
- data/lib/rapns/apns/device_token_format_validator.rb +12 -0
- data/lib/rapns/apns/feedback.rb +14 -0
- data/lib/rapns/apns/notification.rb +84 -0
- data/lib/rapns/app.rb +5 -6
- data/lib/rapns/{config.rb → configuration.rb} +5 -5
- data/lib/rapns/daemon/apns/app_runner.rb +36 -0
- data/lib/rapns/daemon/apns/connection.rb +113 -0
- data/lib/rapns/daemon/apns/delivery.rb +63 -0
- data/lib/rapns/daemon/apns/delivery_handler.rb +21 -0
- data/lib/rapns/daemon/apns/disconnection_error.rb +20 -0
- data/lib/rapns/daemon/apns/feedback_receiver.rb +74 -0
- data/lib/rapns/daemon/app_runner.rb +76 -77
- data/lib/rapns/daemon/database_reconnectable.rb +3 -3
- data/lib/rapns/daemon/delivery.rb +43 -0
- data/lib/rapns/daemon/delivery_error.rb +6 -2
- data/lib/rapns/daemon/delivery_handler.rb +13 -79
- data/lib/rapns/daemon/delivery_queue_18.rb +2 -2
- data/lib/rapns/daemon/delivery_queue_19.rb +3 -3
- data/lib/rapns/daemon/feeder.rb +5 -5
- data/lib/rapns/daemon/gcm/app_runner.rb +13 -0
- data/lib/rapns/daemon/gcm/delivery.rb +206 -0
- data/lib/rapns/daemon/gcm/delivery_handler.rb +20 -0
- data/lib/rapns/daemon.rb +31 -20
- data/lib/rapns/gcm/app.rb +7 -0
- data/lib/rapns/gcm/expiry_collapse_key_mutual_inclusion_validator.rb +11 -0
- data/lib/rapns/gcm/notification.rb +31 -0
- data/lib/rapns/gcm/payload_size_validator.rb +13 -0
- data/lib/rapns/multi_json_helper.rb +16 -0
- data/lib/rapns/notification.rb +28 -95
- data/lib/rapns/version.rb +1 -1
- data/lib/rapns.rb +14 -4
- data/lib/tasks/cane.rake +19 -0
- data/lib/tasks/test.rake +34 -0
- data/spec/acceptance/gcm_upgrade_spec.rb +34 -0
- data/spec/acceptance_spec_helper.rb +85 -0
- data/spec/support/simplecov_helper.rb +13 -0
- data/spec/support/simplecov_quality_formatter.rb +8 -0
- data/spec/unit/apns/app_spec.rb +15 -0
- data/spec/unit/apns/feedback_spec.rb +12 -0
- data/spec/{rapns → unit/apns}/notification_spec.rb +44 -72
- data/spec/unit/app_spec.rb +18 -0
- data/spec/unit/daemon/apns/app_runner_spec.rb +37 -0
- data/spec/{rapns/daemon → unit/daemon/apns}/connection_spec.rb +9 -9
- data/spec/unit/daemon/apns/delivery_handler_spec.rb +48 -0
- data/spec/unit/daemon/apns/delivery_spec.rb +154 -0
- data/spec/{rapns/daemon → unit/daemon/apns}/feedback_receiver_spec.rb +14 -14
- data/spec/unit/daemon/app_runner_shared.rb +66 -0
- data/spec/unit/daemon/app_runner_spec.rb +78 -0
- data/spec/{rapns → unit}/daemon/database_reconnectable_spec.rb +4 -5
- data/spec/{rapns → unit}/daemon/delivery_error_spec.rb +2 -2
- data/spec/unit/daemon/delivery_handler_shared.rb +19 -0
- data/spec/{rapns → unit}/daemon/delivery_queue_spec.rb +1 -1
- data/spec/{rapns → unit}/daemon/feeder_spec.rb +33 -33
- data/spec/unit/daemon/gcm/app_runner_spec.rb +15 -0
- data/spec/unit/daemon/gcm/delivery_handler_spec.rb +36 -0
- data/spec/unit/daemon/gcm/delivery_spec.rb +236 -0
- data/spec/{rapns → unit}/daemon/interruptible_sleep_spec.rb +1 -1
- data/spec/{rapns → unit}/daemon/logger_spec.rb +1 -1
- data/spec/{rapns → unit}/daemon_spec.rb +1 -1
- data/spec/unit/gcm/app_spec.rb +5 -0
- data/spec/unit/gcm/notification_spec.rb +55 -0
- data/spec/unit/notification_shared.rb +38 -0
- data/spec/unit/notification_spec.rb +6 -0
- data/spec/{rapns/app_spec.rb → unit_spec_helper.rb} +76 -16
- metadata +107 -45
- data/lib/rapns/binary_notification_validator.rb +0 -10
- data/lib/rapns/daemon/connection.rb +0 -114
- data/lib/rapns/daemon/delivery_handler_pool.rb +0 -18
- data/lib/rapns/daemon/disconnection_error.rb +0 -14
- data/lib/rapns/daemon/feedback_receiver.rb +0 -82
- data/lib/rapns/device_token_format_validator.rb +0 -10
- data/lib/rapns/feedback.rb +0 -12
- data/spec/rapns/daemon/app_runner_spec.rb +0 -193
- data/spec/rapns/daemon/delivery_handler_pool_spec.rb +0 -17
- data/spec/rapns/daemon/delivery_handler_spec.rb +0 -206
- data/spec/rapns/feedback_spec.rb +0 -12
- data/spec/spec_helper.rb +0 -78
@@ -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 => "
|
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,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,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 :
|
5
|
+
attr_accessible :name, :environment, :certificate, :password, :connections, :auth_key
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
validates :
|
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,
|
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::
|
3
|
+
# A globally accessible instance of Rapns::Configuration
|
4
4
|
def self.configuration
|
5
|
-
@configuration ||= Rapns::
|
5
|
+
@configuration ||= Rapns::Configuration.new
|
6
6
|
end
|
7
7
|
|
8
|
-
# Call the given block yielding to it the global Rapns::
|
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
|
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
|