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.
Files changed (145) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +99 -0
  3. data/LICENSE +7 -0
  4. data/README.md +189 -0
  5. data/bin/rpush +36 -0
  6. data/config/database.yml +44 -0
  7. data/lib/generators/rpush_generator.rb +44 -0
  8. data/lib/generators/templates/add_adm.rb +23 -0
  9. data/lib/generators/templates/add_alert_is_json_to_rapns_notifications.rb +9 -0
  10. data/lib/generators/templates/add_app_to_rapns.rb +11 -0
  11. data/lib/generators/templates/add_fail_after_to_rpush_notifications.rb +9 -0
  12. data/lib/generators/templates/add_gcm.rb +102 -0
  13. data/lib/generators/templates/add_rpush.rb +349 -0
  14. data/lib/generators/templates/add_wpns.rb +16 -0
  15. data/lib/generators/templates/create_rapns_apps.rb +16 -0
  16. data/lib/generators/templates/create_rapns_feedback.rb +18 -0
  17. data/lib/generators/templates/create_rapns_notifications.rb +29 -0
  18. data/lib/generators/templates/rename_rapns_to_rpush.rb +63 -0
  19. data/lib/generators/templates/rpush.rb +104 -0
  20. data/lib/rpush.rb +62 -0
  21. data/lib/rpush/TODO +3 -0
  22. data/lib/rpush/adm/app.rb +15 -0
  23. data/lib/rpush/adm/data_validator.rb +11 -0
  24. data/lib/rpush/adm/notification.rb +29 -0
  25. data/lib/rpush/apns/app.rb +29 -0
  26. data/lib/rpush/apns/binary_notification_validator.rb +12 -0
  27. data/lib/rpush/apns/device_token_format_validator.rb +12 -0
  28. data/lib/rpush/apns/feedback.rb +16 -0
  29. data/lib/rpush/apns/notification.rb +84 -0
  30. data/lib/rpush/apns_feedback.rb +13 -0
  31. data/lib/rpush/app.rb +18 -0
  32. data/lib/rpush/configuration.rb +75 -0
  33. data/lib/rpush/daemon.rb +140 -0
  34. data/lib/rpush/daemon/adm.rb +9 -0
  35. data/lib/rpush/daemon/adm/delivery.rb +222 -0
  36. data/lib/rpush/daemon/apns.rb +16 -0
  37. data/lib/rpush/daemon/apns/certificate_expired_error.rb +20 -0
  38. data/lib/rpush/daemon/apns/delivery.rb +64 -0
  39. data/lib/rpush/daemon/apns/disconnection_error.rb +20 -0
  40. data/lib/rpush/daemon/apns/feedback_receiver.rb +79 -0
  41. data/lib/rpush/daemon/app_runner.rb +187 -0
  42. data/lib/rpush/daemon/batch.rb +115 -0
  43. data/lib/rpush/daemon/constants.rb +59 -0
  44. data/lib/rpush/daemon/delivery.rb +28 -0
  45. data/lib/rpush/daemon/delivery_error.rb +19 -0
  46. data/lib/rpush/daemon/dispatcher/http.rb +21 -0
  47. data/lib/rpush/daemon/dispatcher/tcp.rb +30 -0
  48. data/lib/rpush/daemon/dispatcher_loop.rb +54 -0
  49. data/lib/rpush/daemon/dispatcher_loop_collection.rb +33 -0
  50. data/lib/rpush/daemon/feeder.rb +68 -0
  51. data/lib/rpush/daemon/gcm.rb +9 -0
  52. data/lib/rpush/daemon/gcm/delivery.rb +222 -0
  53. data/lib/rpush/daemon/interruptible_sleep.rb +61 -0
  54. data/lib/rpush/daemon/loggable.rb +31 -0
  55. data/lib/rpush/daemon/reflectable.rb +13 -0
  56. data/lib/rpush/daemon/retry_header_parser.rb +23 -0
  57. data/lib/rpush/daemon/retryable_error.rb +20 -0
  58. data/lib/rpush/daemon/service_config_methods.rb +33 -0
  59. data/lib/rpush/daemon/store/active_record.rb +154 -0
  60. data/lib/rpush/daemon/store/active_record/reconnectable.rb +68 -0
  61. data/lib/rpush/daemon/tcp_connection.rb +143 -0
  62. data/lib/rpush/daemon/too_many_requests_error.rb +20 -0
  63. data/lib/rpush/daemon/wpns.rb +9 -0
  64. data/lib/rpush/daemon/wpns/delivery.rb +132 -0
  65. data/lib/rpush/deprecatable.rb +23 -0
  66. data/lib/rpush/deprecation.rb +23 -0
  67. data/lib/rpush/embed.rb +28 -0
  68. data/lib/rpush/gcm/app.rb +11 -0
  69. data/lib/rpush/gcm/expiry_collapse_key_mutual_inclusion_validator.rb +11 -0
  70. data/lib/rpush/gcm/notification.rb +30 -0
  71. data/lib/rpush/logger.rb +63 -0
  72. data/lib/rpush/multi_json_helper.rb +16 -0
  73. data/lib/rpush/notification.rb +69 -0
  74. data/lib/rpush/notifier.rb +52 -0
  75. data/lib/rpush/payload_data_size_validator.rb +10 -0
  76. data/lib/rpush/push.rb +16 -0
  77. data/lib/rpush/railtie.rb +11 -0
  78. data/lib/rpush/reflection.rb +58 -0
  79. data/lib/rpush/registration_ids_count_validator.rb +10 -0
  80. data/lib/rpush/version.rb +3 -0
  81. data/lib/rpush/wpns/app.rb +9 -0
  82. data/lib/rpush/wpns/notification.rb +26 -0
  83. data/lib/tasks/cane.rake +18 -0
  84. data/lib/tasks/rpush.rake +16 -0
  85. data/lib/tasks/test.rake +38 -0
  86. data/spec/functional/adm_spec.rb +43 -0
  87. data/spec/functional/apns_spec.rb +58 -0
  88. data/spec/functional/embed_spec.rb +49 -0
  89. data/spec/functional/gcm_spec.rb +42 -0
  90. data/spec/functional/wpns_spec.rb +41 -0
  91. data/spec/support/cert_with_password.pem +90 -0
  92. data/spec/support/cert_without_password.pem +59 -0
  93. data/spec/support/install.sh +32 -0
  94. data/spec/support/simplecov_helper.rb +20 -0
  95. data/spec/support/simplecov_quality_formatter.rb +8 -0
  96. data/spec/tmp/.gitkeep +0 -0
  97. data/spec/unit/adm/app_spec.rb +58 -0
  98. data/spec/unit/adm/notification_spec.rb +45 -0
  99. data/spec/unit/apns/app_spec.rb +29 -0
  100. data/spec/unit/apns/feedback_spec.rb +9 -0
  101. data/spec/unit/apns/notification_spec.rb +208 -0
  102. data/spec/unit/apns_feedback_spec.rb +21 -0
  103. data/spec/unit/app_spec.rb +30 -0
  104. data/spec/unit/configuration_spec.rb +45 -0
  105. data/spec/unit/daemon/adm/delivery_spec.rb +243 -0
  106. data/spec/unit/daemon/apns/certificate_expired_error_spec.rb +11 -0
  107. data/spec/unit/daemon/apns/delivery_spec.rb +101 -0
  108. data/spec/unit/daemon/apns/disconnection_error_spec.rb +18 -0
  109. data/spec/unit/daemon/apns/feedback_receiver_spec.rb +117 -0
  110. data/spec/unit/daemon/app_runner_spec.rb +292 -0
  111. data/spec/unit/daemon/batch_spec.rb +232 -0
  112. data/spec/unit/daemon/delivery_error_spec.rb +13 -0
  113. data/spec/unit/daemon/delivery_spec.rb +38 -0
  114. data/spec/unit/daemon/dispatcher/http_spec.rb +33 -0
  115. data/spec/unit/daemon/dispatcher/tcp_spec.rb +38 -0
  116. data/spec/unit/daemon/dispatcher_loop_collection_spec.rb +37 -0
  117. data/spec/unit/daemon/dispatcher_loop_spec.rb +71 -0
  118. data/spec/unit/daemon/feeder_spec.rb +98 -0
  119. data/spec/unit/daemon/gcm/delivery_spec.rb +310 -0
  120. data/spec/unit/daemon/interruptible_sleep_spec.rb +68 -0
  121. data/spec/unit/daemon/reflectable_spec.rb +27 -0
  122. data/spec/unit/daemon/retryable_error_spec.rb +14 -0
  123. data/spec/unit/daemon/service_config_methods_spec.rb +33 -0
  124. data/spec/unit/daemon/store/active_record/reconnectable_spec.rb +114 -0
  125. data/spec/unit/daemon/store/active_record_spec.rb +357 -0
  126. data/spec/unit/daemon/tcp_connection_spec.rb +287 -0
  127. data/spec/unit/daemon/too_many_requests_error_spec.rb +14 -0
  128. data/spec/unit/daemon/wpns/delivery_spec.rb +159 -0
  129. data/spec/unit/daemon_spec.rb +159 -0
  130. data/spec/unit/deprecatable_spec.rb +32 -0
  131. data/spec/unit/deprecation_spec.rb +15 -0
  132. data/spec/unit/embed_spec.rb +50 -0
  133. data/spec/unit/gcm/app_spec.rb +4 -0
  134. data/spec/unit/gcm/notification_spec.rb +36 -0
  135. data/spec/unit/logger_spec.rb +127 -0
  136. data/spec/unit/notification_shared.rb +105 -0
  137. data/spec/unit/notification_spec.rb +15 -0
  138. data/spec/unit/notifier_spec.rb +49 -0
  139. data/spec/unit/push_spec.rb +43 -0
  140. data/spec/unit/reflection_spec.rb +30 -0
  141. data/spec/unit/rpush_spec.rb +9 -0
  142. data/spec/unit/wpns/app_spec.rb +4 -0
  143. data/spec/unit/wpns/notification_spec.rb +30 -0
  144. data/spec/unit_spec_helper.rb +101 -0
  145. 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
@@ -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
@@ -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,9 @@
1
+ module Rpush
2
+ module Daemon
3
+ module Adm
4
+ extend ServiceConfigMethods
5
+
6
+ dispatcher :http
7
+ end
8
+ end
9
+ 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