rpush 1.0.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +99 -0
- data/LICENSE +7 -0
- data/README.md +189 -0
- data/bin/rpush +36 -0
- data/config/database.yml +44 -0
- data/lib/generators/rpush_generator.rb +44 -0
- data/lib/generators/templates/add_adm.rb +23 -0
- data/lib/generators/templates/add_alert_is_json_to_rapns_notifications.rb +9 -0
- data/lib/generators/templates/add_app_to_rapns.rb +11 -0
- data/lib/generators/templates/add_fail_after_to_rpush_notifications.rb +9 -0
- data/lib/generators/templates/add_gcm.rb +102 -0
- data/lib/generators/templates/add_rpush.rb +349 -0
- data/lib/generators/templates/add_wpns.rb +16 -0
- data/lib/generators/templates/create_rapns_apps.rb +16 -0
- data/lib/generators/templates/create_rapns_feedback.rb +18 -0
- data/lib/generators/templates/create_rapns_notifications.rb +29 -0
- data/lib/generators/templates/rename_rapns_to_rpush.rb +63 -0
- data/lib/generators/templates/rpush.rb +104 -0
- data/lib/rpush.rb +62 -0
- data/lib/rpush/TODO +3 -0
- data/lib/rpush/adm/app.rb +15 -0
- data/lib/rpush/adm/data_validator.rb +11 -0
- data/lib/rpush/adm/notification.rb +29 -0
- data/lib/rpush/apns/app.rb +29 -0
- data/lib/rpush/apns/binary_notification_validator.rb +12 -0
- data/lib/rpush/apns/device_token_format_validator.rb +12 -0
- data/lib/rpush/apns/feedback.rb +16 -0
- data/lib/rpush/apns/notification.rb +84 -0
- data/lib/rpush/apns_feedback.rb +13 -0
- data/lib/rpush/app.rb +18 -0
- data/lib/rpush/configuration.rb +75 -0
- data/lib/rpush/daemon.rb +140 -0
- data/lib/rpush/daemon/adm.rb +9 -0
- data/lib/rpush/daemon/adm/delivery.rb +222 -0
- data/lib/rpush/daemon/apns.rb +16 -0
- data/lib/rpush/daemon/apns/certificate_expired_error.rb +20 -0
- data/lib/rpush/daemon/apns/delivery.rb +64 -0
- data/lib/rpush/daemon/apns/disconnection_error.rb +20 -0
- data/lib/rpush/daemon/apns/feedback_receiver.rb +79 -0
- data/lib/rpush/daemon/app_runner.rb +187 -0
- data/lib/rpush/daemon/batch.rb +115 -0
- data/lib/rpush/daemon/constants.rb +59 -0
- data/lib/rpush/daemon/delivery.rb +28 -0
- data/lib/rpush/daemon/delivery_error.rb +19 -0
- data/lib/rpush/daemon/dispatcher/http.rb +21 -0
- data/lib/rpush/daemon/dispatcher/tcp.rb +30 -0
- data/lib/rpush/daemon/dispatcher_loop.rb +54 -0
- data/lib/rpush/daemon/dispatcher_loop_collection.rb +33 -0
- data/lib/rpush/daemon/feeder.rb +68 -0
- data/lib/rpush/daemon/gcm.rb +9 -0
- data/lib/rpush/daemon/gcm/delivery.rb +222 -0
- data/lib/rpush/daemon/interruptible_sleep.rb +61 -0
- data/lib/rpush/daemon/loggable.rb +31 -0
- data/lib/rpush/daemon/reflectable.rb +13 -0
- data/lib/rpush/daemon/retry_header_parser.rb +23 -0
- data/lib/rpush/daemon/retryable_error.rb +20 -0
- data/lib/rpush/daemon/service_config_methods.rb +33 -0
- data/lib/rpush/daemon/store/active_record.rb +154 -0
- data/lib/rpush/daemon/store/active_record/reconnectable.rb +68 -0
- data/lib/rpush/daemon/tcp_connection.rb +143 -0
- data/lib/rpush/daemon/too_many_requests_error.rb +20 -0
- data/lib/rpush/daemon/wpns.rb +9 -0
- data/lib/rpush/daemon/wpns/delivery.rb +132 -0
- data/lib/rpush/deprecatable.rb +23 -0
- data/lib/rpush/deprecation.rb +23 -0
- data/lib/rpush/embed.rb +28 -0
- data/lib/rpush/gcm/app.rb +11 -0
- data/lib/rpush/gcm/expiry_collapse_key_mutual_inclusion_validator.rb +11 -0
- data/lib/rpush/gcm/notification.rb +30 -0
- data/lib/rpush/logger.rb +63 -0
- data/lib/rpush/multi_json_helper.rb +16 -0
- data/lib/rpush/notification.rb +69 -0
- data/lib/rpush/notifier.rb +52 -0
- data/lib/rpush/payload_data_size_validator.rb +10 -0
- data/lib/rpush/push.rb +16 -0
- data/lib/rpush/railtie.rb +11 -0
- data/lib/rpush/reflection.rb +58 -0
- data/lib/rpush/registration_ids_count_validator.rb +10 -0
- data/lib/rpush/version.rb +3 -0
- data/lib/rpush/wpns/app.rb +9 -0
- data/lib/rpush/wpns/notification.rb +26 -0
- data/lib/tasks/cane.rake +18 -0
- data/lib/tasks/rpush.rake +16 -0
- data/lib/tasks/test.rake +38 -0
- data/spec/functional/adm_spec.rb +43 -0
- data/spec/functional/apns_spec.rb +58 -0
- data/spec/functional/embed_spec.rb +49 -0
- data/spec/functional/gcm_spec.rb +42 -0
- data/spec/functional/wpns_spec.rb +41 -0
- data/spec/support/cert_with_password.pem +90 -0
- data/spec/support/cert_without_password.pem +59 -0
- data/spec/support/install.sh +32 -0
- data/spec/support/simplecov_helper.rb +20 -0
- data/spec/support/simplecov_quality_formatter.rb +8 -0
- data/spec/tmp/.gitkeep +0 -0
- data/spec/unit/adm/app_spec.rb +58 -0
- data/spec/unit/adm/notification_spec.rb +45 -0
- data/spec/unit/apns/app_spec.rb +29 -0
- data/spec/unit/apns/feedback_spec.rb +9 -0
- data/spec/unit/apns/notification_spec.rb +208 -0
- data/spec/unit/apns_feedback_spec.rb +21 -0
- data/spec/unit/app_spec.rb +30 -0
- data/spec/unit/configuration_spec.rb +45 -0
- data/spec/unit/daemon/adm/delivery_spec.rb +243 -0
- data/spec/unit/daemon/apns/certificate_expired_error_spec.rb +11 -0
- data/spec/unit/daemon/apns/delivery_spec.rb +101 -0
- data/spec/unit/daemon/apns/disconnection_error_spec.rb +18 -0
- data/spec/unit/daemon/apns/feedback_receiver_spec.rb +117 -0
- data/spec/unit/daemon/app_runner_spec.rb +292 -0
- data/spec/unit/daemon/batch_spec.rb +232 -0
- data/spec/unit/daemon/delivery_error_spec.rb +13 -0
- data/spec/unit/daemon/delivery_spec.rb +38 -0
- data/spec/unit/daemon/dispatcher/http_spec.rb +33 -0
- data/spec/unit/daemon/dispatcher/tcp_spec.rb +38 -0
- data/spec/unit/daemon/dispatcher_loop_collection_spec.rb +37 -0
- data/spec/unit/daemon/dispatcher_loop_spec.rb +71 -0
- data/spec/unit/daemon/feeder_spec.rb +98 -0
- data/spec/unit/daemon/gcm/delivery_spec.rb +310 -0
- data/spec/unit/daemon/interruptible_sleep_spec.rb +68 -0
- data/spec/unit/daemon/reflectable_spec.rb +27 -0
- data/spec/unit/daemon/retryable_error_spec.rb +14 -0
- data/spec/unit/daemon/service_config_methods_spec.rb +33 -0
- data/spec/unit/daemon/store/active_record/reconnectable_spec.rb +114 -0
- data/spec/unit/daemon/store/active_record_spec.rb +357 -0
- data/spec/unit/daemon/tcp_connection_spec.rb +287 -0
- data/spec/unit/daemon/too_many_requests_error_spec.rb +14 -0
- data/spec/unit/daemon/wpns/delivery_spec.rb +159 -0
- data/spec/unit/daemon_spec.rb +159 -0
- data/spec/unit/deprecatable_spec.rb +32 -0
- data/spec/unit/deprecation_spec.rb +15 -0
- data/spec/unit/embed_spec.rb +50 -0
- data/spec/unit/gcm/app_spec.rb +4 -0
- data/spec/unit/gcm/notification_spec.rb +36 -0
- data/spec/unit/logger_spec.rb +127 -0
- data/spec/unit/notification_shared.rb +105 -0
- data/spec/unit/notification_spec.rb +15 -0
- data/spec/unit/notifier_spec.rb +49 -0
- data/spec/unit/push_spec.rb +43 -0
- data/spec/unit/reflection_spec.rb +30 -0
- data/spec/unit/rpush_spec.rb +9 -0
- data/spec/unit/wpns/app_spec.rb +4 -0
- data/spec/unit/wpns/notification_spec.rb +30 -0
- data/spec/unit_spec_helper.rb +101 -0
- metadata +276 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
module Rpush
|
|
2
|
+
module Apns
|
|
3
|
+
class Notification < Rpush::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 Rpush::Apns::DeviceTokenFormatValidator
|
|
10
|
+
validates_with Rpush::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 = '__rpush_mdm__'
|
|
44
|
+
def mdm=(magic)
|
|
45
|
+
self.attributes_for_device = (attributes_for_device || {}).merge({ MDM_KEY => magic })
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
CONTENT_AVAILABLE_KEY = '__rpush_content_available__'
|
|
49
|
+
def content_available=(bool)
|
|
50
|
+
return unless bool
|
|
51
|
+
self.attributes_for_device = (attributes_for_device || {}).merge({ 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 }
|
|
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
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Rpush
|
|
2
|
+
def self.apns_feedback
|
|
3
|
+
Rpush.require_for_daemon
|
|
4
|
+
Rpush::Daemon.initialize_store
|
|
5
|
+
|
|
6
|
+
Rpush::Apns::App.all.each do |app|
|
|
7
|
+
receiver = Rpush::Daemon::Apns::FeedbackReceiver.new(app)
|
|
8
|
+
receiver.check_for_feedback
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
nil
|
|
12
|
+
end
|
|
13
|
+
end
|
data/lib/rpush/app.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Rpush
|
|
2
|
+
class App < ActiveRecord::Base
|
|
3
|
+
self.table_name = 'rpush_apps'
|
|
4
|
+
|
|
5
|
+
if Rpush.attr_accessible_available?
|
|
6
|
+
attr_accessible :name, :environment, :certificate, :password, :connections, :auth_key, :client_id, :client_secret
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
has_many :notifications, :class_name => 'Rpush::Notification', :dependent => :destroy
|
|
10
|
+
|
|
11
|
+
validates :name, :presence => true, :uniqueness => { :scope => [:type, :environment] }
|
|
12
|
+
validates_numericality_of :connections, :greater_than => 0, :only_integer => true
|
|
13
|
+
|
|
14
|
+
def service_name
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module Rpush
|
|
2
|
+
def self.config
|
|
3
|
+
@config ||= Rpush::Configuration.new
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
def self.configure
|
|
7
|
+
yield config if block_given?
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
CONFIG_ATTRS = [:foreground, :push_poll, :feedback_poll, :embedded,
|
|
11
|
+
:check_for_errors, :pid_file, :batch_size, :push, :store, :logger,
|
|
12
|
+
:batch_storage_updates, :wakeup]
|
|
13
|
+
|
|
14
|
+
class ConfigurationWithoutDefaults < Struct.new(*CONFIG_ATTRS)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class Configuration < Struct.new(*CONFIG_ATTRS)
|
|
18
|
+
include Deprecatable
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
super
|
|
22
|
+
set_defaults
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def update(other)
|
|
26
|
+
CONFIG_ATTRS.each do |attr|
|
|
27
|
+
other_value = other.send(attr)
|
|
28
|
+
send("#{attr}=", other_value) unless other_value.nil?
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def pid_file=(path)
|
|
33
|
+
if path && !Pathname.new(path).absolute?
|
|
34
|
+
super(File.join(Rails.root, path))
|
|
35
|
+
else
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def logger=(logger)
|
|
41
|
+
super(logger)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def foreground=(bool)
|
|
45
|
+
if Rpush.jruby?
|
|
46
|
+
# The JVM does not support fork().
|
|
47
|
+
super(true)
|
|
48
|
+
else
|
|
49
|
+
super
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def set_defaults
|
|
54
|
+
if Rpush.jruby?
|
|
55
|
+
# The JVM does not support fork().
|
|
56
|
+
self.foreground = true
|
|
57
|
+
else
|
|
58
|
+
self.foreground = false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
self.push_poll = 2
|
|
62
|
+
self.feedback_poll = 60
|
|
63
|
+
self.check_for_errors = true
|
|
64
|
+
self.batch_size = 100
|
|
65
|
+
self.pid_file = nil
|
|
66
|
+
self.store = :active_record
|
|
67
|
+
self.logger = nil
|
|
68
|
+
self.batch_storage_updates = true
|
|
69
|
+
|
|
70
|
+
# Internal options.
|
|
71
|
+
self.embedded = false
|
|
72
|
+
self.push = false
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
data/lib/rpush/daemon.rb
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
require 'thread'
|
|
2
|
+
require 'socket'
|
|
3
|
+
require 'pathname'
|
|
4
|
+
require 'openssl'
|
|
5
|
+
|
|
6
|
+
require 'net/http/persistent'
|
|
7
|
+
|
|
8
|
+
require 'rpush/daemon/constants'
|
|
9
|
+
require 'rpush/daemon/reflectable'
|
|
10
|
+
require 'rpush/daemon/loggable'
|
|
11
|
+
require 'rpush/daemon/interruptible_sleep'
|
|
12
|
+
require 'rpush/daemon/delivery_error'
|
|
13
|
+
require 'rpush/daemon/retryable_error'
|
|
14
|
+
require 'rpush/daemon/too_many_requests_error'
|
|
15
|
+
require 'rpush/daemon/delivery'
|
|
16
|
+
require 'rpush/daemon/feeder'
|
|
17
|
+
require 'rpush/daemon/batch'
|
|
18
|
+
require 'rpush/daemon/app_runner'
|
|
19
|
+
require 'rpush/daemon/tcp_connection'
|
|
20
|
+
require 'rpush/daemon/dispatcher_loop'
|
|
21
|
+
require 'rpush/daemon/dispatcher_loop_collection'
|
|
22
|
+
require 'rpush/daemon/dispatcher/http'
|
|
23
|
+
require 'rpush/daemon/dispatcher/tcp'
|
|
24
|
+
require 'rpush/daemon/service_config_methods'
|
|
25
|
+
require 'rpush/daemon/retry_header_parser'
|
|
26
|
+
|
|
27
|
+
require 'rpush/daemon/apns/delivery'
|
|
28
|
+
require 'rpush/daemon/apns/disconnection_error'
|
|
29
|
+
require 'rpush/daemon/apns/certificate_expired_error'
|
|
30
|
+
require 'rpush/daemon/apns/feedback_receiver'
|
|
31
|
+
require 'rpush/daemon/apns'
|
|
32
|
+
|
|
33
|
+
require 'rpush/daemon/gcm/delivery'
|
|
34
|
+
require 'rpush/daemon/gcm'
|
|
35
|
+
|
|
36
|
+
require 'rpush/daemon/wpns/delivery'
|
|
37
|
+
require 'rpush/daemon/wpns'
|
|
38
|
+
|
|
39
|
+
require 'rpush/daemon/adm/delivery'
|
|
40
|
+
require 'rpush/daemon/adm'
|
|
41
|
+
|
|
42
|
+
module Rpush
|
|
43
|
+
module Daemon
|
|
44
|
+
class << self
|
|
45
|
+
attr_accessor :store
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.start
|
|
49
|
+
setup_signal_traps if trap_signals?
|
|
50
|
+
|
|
51
|
+
initialize_store
|
|
52
|
+
return unless store
|
|
53
|
+
|
|
54
|
+
if daemonize?
|
|
55
|
+
daemonize
|
|
56
|
+
store.after_daemonize
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
write_pid_file
|
|
60
|
+
AppRunner.sync
|
|
61
|
+
Feeder.start
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.shutdown(quiet = false)
|
|
65
|
+
puts "\nShutting down..." unless quiet
|
|
66
|
+
Feeder.stop
|
|
67
|
+
AppRunner.stop
|
|
68
|
+
delete_pid_file
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.initialize_store
|
|
72
|
+
return if store
|
|
73
|
+
begin
|
|
74
|
+
name = Rpush.config.store.to_s
|
|
75
|
+
require "rpush/daemon/store/#{name}"
|
|
76
|
+
self.store = Rpush::Daemon::Store.const_get(name.camelcase).new
|
|
77
|
+
rescue StandardError, LoadError => e
|
|
78
|
+
Rpush.logger.error("Failed to load '#{Rpush.config.store}' storage backend.")
|
|
79
|
+
Rpush.logger.error(e)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
protected
|
|
84
|
+
|
|
85
|
+
def self.daemonize?
|
|
86
|
+
!(Rpush.config.foreground || Rpush.config.embedded || Rpush.jruby?)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.trap_signals?
|
|
90
|
+
!Rpush.config.embedded
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.setup_signal_traps
|
|
94
|
+
@shutting_down = false
|
|
95
|
+
|
|
96
|
+
Signal.trap('SIGHUP') { AppRunner.sync }
|
|
97
|
+
Signal.trap('SIGUSR2') { AppRunner.debug }
|
|
98
|
+
|
|
99
|
+
['SIGINT', 'SIGTERM'].each do |signal|
|
|
100
|
+
Signal.trap(signal) { handle_shutdown_signal }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def self.handle_shutdown_signal
|
|
105
|
+
exit 1 if @shutting_down
|
|
106
|
+
@shutting_down = true
|
|
107
|
+
shutdown
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def self.write_pid_file
|
|
111
|
+
if !Rpush.config.pid_file.blank?
|
|
112
|
+
begin
|
|
113
|
+
File.open(Rpush.config.pid_file, 'w') { |f| f.puts Process.pid }
|
|
114
|
+
rescue SystemCallError => e
|
|
115
|
+
Rpush.logger.error("Failed to write PID to '#{Rpush.config.pid_file}': #{e.inspect}")
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def self.delete_pid_file
|
|
121
|
+
pid_file = Rpush.config.pid_file
|
|
122
|
+
File.delete(pid_file) if !pid_file.blank? && File.exists?(pid_file)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# :nocov:
|
|
126
|
+
def self.daemonize
|
|
127
|
+
if RUBY_VERSION < "1.9"
|
|
128
|
+
exit if fork
|
|
129
|
+
Process.setsid
|
|
130
|
+
exit if fork
|
|
131
|
+
Dir.chdir "/"
|
|
132
|
+
STDIN.reopen "/dev/null"
|
|
133
|
+
STDOUT.reopen "/dev/null", "a"
|
|
134
|
+
STDERR.reopen "/dev/null", "a"
|
|
135
|
+
else
|
|
136
|
+
Process.daemon
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
module Rpush
|
|
2
|
+
module Daemon
|
|
3
|
+
module Adm
|
|
4
|
+
# https://developer.amazon.com/sdk/adm/sending-message.html
|
|
5
|
+
class Delivery < Rpush::Daemon::Delivery
|
|
6
|
+
include MultiJsonHelper
|
|
7
|
+
|
|
8
|
+
# Oauth2.0 token endpoint. This endpoint is used to request authorization tokens.
|
|
9
|
+
AMAZON_TOKEN_URI = URI.parse('https://api.amazon.com/auth/O2/token')
|
|
10
|
+
|
|
11
|
+
# ADM services endpoint. This endpoint is used to perform ADM requests.
|
|
12
|
+
AMAZON_ADM_URL = 'https://api.amazon.com/messaging/registrations/%s/messages'
|
|
13
|
+
|
|
14
|
+
# Data used to request authorization tokens.
|
|
15
|
+
ACCESS_TOKEN_REQUEST_DATA = {"grant_type" => "client_credentials", "scope" => "messaging:push"}
|
|
16
|
+
|
|
17
|
+
def initialize(app, http, notification, batch)
|
|
18
|
+
@app = app
|
|
19
|
+
@http = http
|
|
20
|
+
@notification = notification
|
|
21
|
+
@batch = batch
|
|
22
|
+
@sent_registration_ids = []
|
|
23
|
+
@failed_registration_ids = {}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def perform
|
|
27
|
+
begin
|
|
28
|
+
@notification.registration_ids.each do |registration_id|
|
|
29
|
+
handle_response(do_post(registration_id), registration_id)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if(@sent_registration_ids.empty?)
|
|
33
|
+
raise Rpush::DeliveryError.new(nil, @notification.id, describe_errors)
|
|
34
|
+
else
|
|
35
|
+
unless(@failed_registration_ids.empty?)
|
|
36
|
+
@notification.error_description = describe_errors
|
|
37
|
+
Rpush::Daemon.store.update_notification(@notification)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
mark_delivered
|
|
41
|
+
end
|
|
42
|
+
rescue Rpush::RetryableError => error
|
|
43
|
+
handle_retryable(error)
|
|
44
|
+
rescue Rpush::TooManyRequestsError => error
|
|
45
|
+
handle_too_many_requests(error)
|
|
46
|
+
rescue Rpush::DeliveryError => error
|
|
47
|
+
mark_failed(error.code, error.description)
|
|
48
|
+
raise
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
protected
|
|
53
|
+
|
|
54
|
+
def handle_response(response, current_registration_id)
|
|
55
|
+
case response.code.to_i
|
|
56
|
+
when 200
|
|
57
|
+
ok(response, current_registration_id)
|
|
58
|
+
when 400
|
|
59
|
+
bad_request(response, current_registration_id)
|
|
60
|
+
when 401
|
|
61
|
+
unauthorized(response)
|
|
62
|
+
when 429
|
|
63
|
+
too_many_requests(response)
|
|
64
|
+
when 500
|
|
65
|
+
internal_server_error(response, current_registration_id)
|
|
66
|
+
when 503
|
|
67
|
+
service_unavailable(response)
|
|
68
|
+
else
|
|
69
|
+
raise Rpush::DeliveryError.new(response.code, @notification.id, Rpush::Daemon::HTTP_STATUS_CODES[response.code.to_i])
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def ok(response, current_registration_id)
|
|
74
|
+
response_body = multi_json_load(response.body)
|
|
75
|
+
|
|
76
|
+
if(response_body.has_key?('registrationID'))
|
|
77
|
+
@sent_registration_ids << response_body['registrationID']
|
|
78
|
+
log_info("#{@notification.id} sent to #{response_body['registrationID']}")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
if(current_registration_id != response_body['registrationID'])
|
|
82
|
+
reflect(:adm_canonical_id, current_registration_id, response_body['registrationID'])
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def handle_retryable(error)
|
|
87
|
+
case error.code
|
|
88
|
+
when 401
|
|
89
|
+
# clear app access_token so a new one is fetched
|
|
90
|
+
@notification.app.access_token = nil
|
|
91
|
+
get_access_token
|
|
92
|
+
mark_retryable(@notification, Time.now) if @notification.app.access_token
|
|
93
|
+
when 503
|
|
94
|
+
retry_delivery(@notification, error.response)
|
|
95
|
+
log_warn("ADM responded with an Service Unavailable Error. " + retry_message)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def handle_too_many_requests(error)
|
|
100
|
+
if(@sent_registration_ids.empty?)
|
|
101
|
+
# none sent yet, just resend after the specified retry-after response.header
|
|
102
|
+
retry_delivery(@notification, error.response)
|
|
103
|
+
else
|
|
104
|
+
# save unsent registration ids
|
|
105
|
+
unsent_registration_ids = @notification.registration_ids.select { |reg_id| !@sent_registration_ids.include?(reg_id) }
|
|
106
|
+
|
|
107
|
+
# update the current notification so it only contains the sent reg ids
|
|
108
|
+
@notification.registration_ids.reject! { |reg_id| !@sent_registration_ids.include?(reg_id) }
|
|
109
|
+
|
|
110
|
+
Rpush::Daemon.store.update_notification(@notification)
|
|
111
|
+
|
|
112
|
+
# create a new notification with the remaining unsent reg ids
|
|
113
|
+
create_new_notification(error.response, unsent_registration_ids)
|
|
114
|
+
|
|
115
|
+
# mark the current notification as sent
|
|
116
|
+
mark_delivered
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def bad_request(response, current_registration_id)
|
|
121
|
+
response_body = multi_json_load(response.body)
|
|
122
|
+
|
|
123
|
+
if(response_body.has_key?('reason'))
|
|
124
|
+
log_warn("bad_request: #{current_registration_id} (#{response_body['reason']})")
|
|
125
|
+
@failed_registration_ids[current_registration_id] = response_body['reason']
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def unauthorized(response)
|
|
130
|
+
# Indicate a notification is retryable. Because ADM requires separate request for each push token, this will safely mark the entire notification to retry delivery.
|
|
131
|
+
raise Rpush::RetryableError.new(response.code.to_i, @notification.id, 'ADM responded with an Unauthorized Error.', response)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def too_many_requests(response)
|
|
135
|
+
# raise error so the current notification stops sending messages to remaining reg ids
|
|
136
|
+
raise Rpush::TooManyRequestsError.new(response.code.to_i, @notification.id, 'Exceeded maximum allowable rate of messages.', response)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def internal_server_error(response, current_registration_id)
|
|
140
|
+
@failed_registration_ids[current_registration_id] = "Internal Server Error"
|
|
141
|
+
log_warn("internal_server_error: #{current_registration_id} (Internal Server Error)")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def service_unavailable(response)
|
|
145
|
+
# Indicate a notification is retryable. Because ADM requires separate request for each push token, this will safely mark the entire notification to retry delivery.
|
|
146
|
+
raise Rpush::RetryableError.new(response.code.to_i, @notification.id, 'ADM responded with an Service Unavailable Error.', response)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def create_new_notification(response, registration_ids)
|
|
150
|
+
attrs = @notification.attributes.slice('app_id', 'collapse_key', 'delay_while_idle')
|
|
151
|
+
Rpush::Daemon.store.create_adm_notification(attrs, @notification.data, registration_ids, deliver_after_header(response), @notification.app)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def deliver_after_header(response)
|
|
155
|
+
Rpush::Daemon::RetryHeaderParser.parse(response.header['retry-after'])
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def retry_delivery(notification, response)
|
|
159
|
+
if time = deliver_after_header(response)
|
|
160
|
+
mark_retryable(notification, time)
|
|
161
|
+
else
|
|
162
|
+
mark_retryable_exponential(notification)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def describe_errors
|
|
167
|
+
if @failed_registration_ids.size == @notification.registration_ids.size
|
|
168
|
+
"Failed to deliver to all recipients."
|
|
169
|
+
else
|
|
170
|
+
error_msgs = []
|
|
171
|
+
@failed_registration_ids.each_pair { |regId, reason| error_msgs.push("#{regId}: #{reason}") }
|
|
172
|
+
"Failed to deliver to recipients: \n#{error_msgs.join("\n")}"
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def retry_message
|
|
177
|
+
"Notification #{@notification.id} will be retired after #{@notification.deliver_after.strftime("%Y-%m-%d %H:%M:%S")} (retry #{@notification.retries})."
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def do_post(registration_id)
|
|
181
|
+
adm_uri = URI.parse(AMAZON_ADM_URL % [registration_id])
|
|
182
|
+
post = Net::HTTP::Post.new(adm_uri.path, initheader = {
|
|
183
|
+
'Content-Type' => 'application/json',
|
|
184
|
+
'Accept' => 'application/json',
|
|
185
|
+
'x-amzn-type-version' => 'com.amazon.device.messaging.ADMMessage@1.0',
|
|
186
|
+
'x-amzn-accept-type' => 'com.amazon.device.messaging.ADMSendResult@1.0',
|
|
187
|
+
'Authorization' => "Bearer #{get_access_token}"
|
|
188
|
+
})
|
|
189
|
+
post.body = @notification.as_json.to_json
|
|
190
|
+
|
|
191
|
+
@http.request(adm_uri, post)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def get_access_token
|
|
195
|
+
if(@notification.app.access_token.nil? || @notification.app.access_token_expired?)
|
|
196
|
+
post = Net::HTTP::Post.new(AMAZON_TOKEN_URI.path, initheader = {'Content-Type' => 'application/x-www-form-urlencoded'})
|
|
197
|
+
post.set_form_data(ACCESS_TOKEN_REQUEST_DATA.merge({'client_id' => @notification.app.client_id, 'client_secret' => @notification.app.client_secret}))
|
|
198
|
+
|
|
199
|
+
handle_access_token(@http.request(AMAZON_TOKEN_URI, post))
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
@notification.app.access_token
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def handle_access_token(response)
|
|
206
|
+
if(response.code.to_i == 200)
|
|
207
|
+
update_access_token(JSON.parse(response.body))
|
|
208
|
+
Rpush::Daemon.store.update_app(@notification.app)
|
|
209
|
+
log_info("ADM access token updated: token = #{@notification.app.access_token}, expires = #{@notification.app.access_token_expiration}")
|
|
210
|
+
else
|
|
211
|
+
log_warn("Could not retrieve access token from ADM: #{response.body}")
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def update_access_token(data)
|
|
216
|
+
@notification.app.access_token = data['access_token']
|
|
217
|
+
@notification.app.access_token_expiration = Time.now + data['expires_in'].to_i
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|