rpush 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,20 @@
1
+ module Rpush
2
+ class TooManyRequestsError < StandardError
3
+ attr_reader :code, :description, :response
4
+
5
+ def initialize(code, notification_id, description, response)
6
+ @code = code
7
+ @notification_id = notification_id
8
+ @description = description
9
+ @response = response
10
+ end
11
+
12
+ def to_s
13
+ message
14
+ end
15
+
16
+ def message
17
+ "Too many requests for #{@notification_id}, received error #{@code} (#{@description}) - retry after #{@response.header['retry-after']}"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,9 @@
1
+ module Rpush
2
+ module Daemon
3
+ module Wpns
4
+ extend ServiceConfigMethods
5
+
6
+ dispatcher :http
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,132 @@
1
+ module Rpush
2
+ module Daemon
3
+ module Wpns
4
+
5
+ # http://msdn.microsoft.com/en-us/library/windowsphone/develop/ff941100%28v=vs.105%29.aspx
6
+ class Delivery < Rpush::Daemon::Delivery
7
+
8
+ FAILURE_MESSAGES = {
9
+ 400 => 'Bad XML or malformed notification URI.',
10
+ 401 => 'Unauthorized to send a notification to this app.'
11
+ }
12
+
13
+ def initialize(app, http, notification, batch)
14
+ @app = app
15
+ @http = http
16
+ @notification = notification
17
+ @batch = batch
18
+ end
19
+
20
+ def perform
21
+ begin
22
+ handle_response(do_post)
23
+ rescue Rpush::DeliveryError => error
24
+ mark_failed(error.code, error.description)
25
+ raise
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def handle_response(response)
32
+ code = response.code.to_i
33
+ case code
34
+ when 200
35
+ ok(response)
36
+ when 406
37
+ not_acceptable(response)
38
+ when 412
39
+ precondition_failed(response)
40
+ when 503
41
+ service_unavailable(response)
42
+ else
43
+ handle_failure(code)
44
+ end
45
+ end
46
+
47
+ def handle_failure(code, msg=nil)
48
+ unless msg
49
+ msg = if FAILURE_MESSAGES.key?(code)
50
+ FAILURE_MESSAGES[code]
51
+ else
52
+ Rpush::Daemon::HTTP_STATUS_CODES[code]
53
+ end
54
+ end
55
+ raise Rpush::DeliveryError.new(code, @notification.id, msg)
56
+ end
57
+
58
+ def ok(response)
59
+ status = status_from_response(response)
60
+ case status[:notification]
61
+ when ["Received"]
62
+ mark_delivered
63
+ log_info("#{@notification.id} sent successfully")
64
+ when ["QueueFull"]
65
+ mark_retryable(@notification, Time.now + (60*10))
66
+ log_warn("#{@notification.id} cannot be sent. The Queue is full.")
67
+ when ["Suppressed"]
68
+ handle_failure(200, "Notification was received but suppressed by the service.")
69
+ end
70
+ end
71
+
72
+ def not_acceptable(response)
73
+ retry_notification("Per-day throttling limit reached.")
74
+ end
75
+
76
+ def precondition_failed(response)
77
+ retry_notification("Device unreachable.")
78
+ end
79
+
80
+ def service_unavailable(response)
81
+ mark_retryable_exponential(@notification)
82
+ log_warn("Service Unavailable. " + retry_message)
83
+ end
84
+
85
+ def retry_message
86
+ "Notification #{@notification.id} will be retried after #{@notification.deliver_after.strftime("%Y-%m-%d %H:%M:%S")} (retry #{@notification.retries})."
87
+ end
88
+
89
+ def retry_notification(reason)
90
+ deliver_after = Time.now + (60*60)
91
+ mark_retryable(@notification, deliver_after)
92
+ log_warn("#{reason} " + retry_message)
93
+ end
94
+
95
+ def do_post
96
+ body = notification_to_xml
97
+ header = {
98
+ "Content-Length" => body.length.to_s,
99
+ "Content-Type" => "text/xml",
100
+ "X-WindowsPhone-Target" => "toast",
101
+ "X-NotificationClass" => '2'
102
+ }
103
+ post = Net::HTTP::Post.new(URI.parse(@notification.uri).path, initheader=header)
104
+ post.body = body
105
+ @http.request(URI.parse(@notification.uri), post)
106
+ end
107
+
108
+ def status_from_response(response)
109
+ headers = response.to_hash
110
+ {
111
+ notification: headers["x-notificationstatus"],
112
+ notification_channel: headers["x-subscriptionstatus"],
113
+ device_connection: headers["x-deviceconnectionstatus"]
114
+ }
115
+ end
116
+
117
+ def notification_to_xml
118
+ msg = @notification.alert.gsub(/&/, "&amp;").gsub(/</, "&lt;") \
119
+ .gsub(/>/, "&gt;").gsub(/'/, "&apos;").gsub(/"/, "&quot;")
120
+ <<-EOF
121
+ <?xml version="1.0" encoding="utf-8"?>
122
+ <wp:Notification xmlns:wp="WPNotification">
123
+ <wp:Toast>
124
+ <wp:Text1>#{msg}</wp:Text1>
125
+ </wp:Toast>
126
+ </wp:Notification>
127
+ EOF
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,23 @@
1
+ module Rpush
2
+ module Deprecatable
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ def deprecated(method_name, version, msg=nil)
9
+ instance_eval do
10
+ alias_method "#{method_name}_without_warning", method_name
11
+ end
12
+ warning = "#{method_name} is deprecated and will be removed from Rpush #{version}."
13
+ warning << " #{msg}" if msg
14
+ class_eval(<<-RUBY, __FILE__, __LINE__)
15
+ def #{method_name}(*args, &blk)
16
+ Rpush::Deprecation.warn(#{warning.inspect})
17
+ #{method_name}_without_warning(*args, &blk)
18
+ end
19
+ RUBY
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module Rpush
2
+ class Deprecation
3
+ def self.muted
4
+ begin
5
+ orig_val = Thread.current[:rpush_mute_deprecations]
6
+ Thread.current[:rpush_mute_deprecations] = true
7
+ yield
8
+ ensure
9
+ Thread.current[:rpush_mute_deprecations] = orig_val
10
+ end
11
+ end
12
+
13
+ def self.muted?
14
+ Thread.current[:rpush_mute_deprecations] == true
15
+ end
16
+
17
+ def self.warn(msg)
18
+ unless Rpush::Deprecation.muted?
19
+ STDERR.puts "DEPRECATION WARNING: #{msg}"
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,28 @@
1
+ module Rpush
2
+ def self.embed(options = {})
3
+ Rpush.require_for_daemon
4
+
5
+ config = Rpush::ConfigurationWithoutDefaults.new
6
+ options.each { |k, v| config.send("#{k}=", v) }
7
+ config.embedded = true
8
+ Rpush.config.update(config)
9
+ Rpush::Daemon.start
10
+
11
+ Kernel.at_exit { shutdown }
12
+ end
13
+
14
+ def self.shutdown
15
+ return unless Rpush.config.embedded
16
+ Rpush::Daemon.shutdown
17
+ end
18
+
19
+ def self.sync
20
+ return unless Rpush.config.embedded
21
+ Rpush::Daemon::AppRunner.sync
22
+ end
23
+
24
+ def self.debug
25
+ return unless Rpush.config.embedded
26
+ Rpush::Daemon::AppRunner.debug
27
+ end
28
+ end
@@ -0,0 +1,11 @@
1
+ module Rpush
2
+ module Gcm
3
+ class App < Rpush::App
4
+ validates :auth_key, :presence => true
5
+
6
+ def service_name
7
+ 'gcm'
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Rpush
2
+ module Gcm
3
+ class ExpiryCollapseKeyMutualInclusionValidator < ActiveModel::Validator
4
+ def validate(record)
5
+ if record.collapse_key && !record.expiry
6
+ record.errors[:expiry] << "must be set when using a collapse_key"
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,30 @@
1
+ module Rpush
2
+ module Gcm
3
+ class Notification < Rpush::Notification
4
+ validates :registration_ids, :presence => true
5
+
6
+ validates_with Rpush::PayloadDataSizeValidator, limit: 4096
7
+ validates_with Rpush::RegistrationIdsCountValidator, limit: 1000
8
+
9
+ validates_with Rpush::Gcm::ExpiryCollapseKeyMutualInclusionValidator
10
+
11
+ def as_json
12
+ json = {
13
+ 'registration_ids' => registration_ids,
14
+ 'delay_while_idle' => delay_while_idle,
15
+ 'data' => data
16
+ }
17
+
18
+ if collapse_key
19
+ json['collapse_key'] = collapse_key
20
+ end
21
+
22
+ if expiry
23
+ json['time_to_live'] = expiry
24
+ end
25
+
26
+ json
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,63 @@
1
+ module Rpush
2
+ class Logger
3
+ def initialize(options)
4
+ @options = options
5
+
6
+ begin
7
+ log_dir = File.join(Rails.root, 'log')
8
+ FileUtils.mkdir_p(log_dir)
9
+ log = File.open(File.join(log_dir, 'rpush.log'), 'a')
10
+ log.sync = true
11
+ setup_logger(log)
12
+ rescue Errno::ENOENT, Errno::EPERM => e
13
+ @logger = nil
14
+ error(e)
15
+ error('Logging disabled.')
16
+ end
17
+ end
18
+
19
+ def info(msg)
20
+ log(:info, msg)
21
+ end
22
+
23
+ def error(msg)
24
+ log(:error, msg, 'ERROR', STDERR)
25
+ end
26
+
27
+ def warn(msg)
28
+ log(:warn, msg, 'WARNING', STDERR)
29
+ end
30
+
31
+ private
32
+
33
+ def setup_logger(log)
34
+ if Rpush.config.logger
35
+ @logger = Rpush.config.logger
36
+ elsif ActiveSupport.const_defined?('BufferedLogger')
37
+ @logger = ActiveSupport::BufferedLogger.new(log, Rails.logger.level)
38
+ @logger.auto_flushing = Rails.logger.respond_to?(:auto_flushing) ? Rails.logger.auto_flushing : true
39
+ else
40
+ @logger = ActiveSupport::Logger.new(log, Rails.logger.level)
41
+ end
42
+ end
43
+
44
+ def log(where, msg, prefix = nil, io = STDOUT)
45
+ if msg.is_a?(Exception)
46
+ formatted_backtrace = msg.backtrace.join("\n")
47
+ msg = "#{msg.class.name}, #{msg.message}\n#{formatted_backtrace}"
48
+ end
49
+
50
+ formatted_msg = "[#{Time.now.to_s(:db)}] "
51
+ formatted_msg << "[#{prefix}] " if prefix
52
+ formatted_msg << msg
53
+
54
+ if io == STDERR
55
+ io.puts formatted_msg
56
+ elsif @options[:foreground]
57
+ io.puts formatted_msg
58
+ end
59
+
60
+ @logger.send(where, formatted_msg) if @logger
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,16 @@
1
+ module Rpush
2
+ module MultiJsonHelper
3
+ def multi_json_load(string, options = {})
4
+ # Calling load on multi_json less than v1.3.0 attempts to load a file from disk.
5
+ if Gem.loaded_specs['multi_json'].version >= Gem::Version.create('1.3.0')
6
+ MultiJson.load(string, options)
7
+ else
8
+ MultiJson.decode(string, options)
9
+ end
10
+ end
11
+
12
+ def multi_json_dump(string, options = {})
13
+ MultiJson.respond_to?(:dump) ? MultiJson.dump(string, options) : MultiJson.encode(string, options)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,69 @@
1
+ module Rpush
2
+ class Notification < ActiveRecord::Base
3
+ include Rpush::MultiJsonHelper
4
+
5
+ self.table_name = 'rpush_notifications'
6
+
7
+ # TODO: Dump using multi json.
8
+ serialize :registration_ids
9
+
10
+ belongs_to :app, :class_name => 'Rpush::App'
11
+
12
+ if Rpush.attr_accessible_available?
13
+ attr_accessible :badge, :device_token, :sound, :alert, :data, :expiry,:delivered,
14
+ :delivered_at, :failed, :failed_at, :error_code, :error_description, :deliver_after,
15
+ :alert_is_json, :app, :app_id, :collapse_key, :delay_while_idle, :registration_ids, :uri
16
+ end
17
+
18
+ validates :expiry, :numericality => true, :allow_nil => true
19
+ validates :app, :presence => true
20
+
21
+ scope :ready_for_delivery, lambda {
22
+ where('delivered = ? AND failed = ? AND (deliver_after IS NULL OR deliver_after < ?)',
23
+ false, false, Time.now)
24
+ }
25
+
26
+ scope :for_apps, lambda { |apps|
27
+ where('app_id IN (?)', apps.map(&:id))
28
+ }
29
+
30
+ scope :completed, lambda { where("delivered = ? OR failed = ?", true, true) }
31
+
32
+ def data=(attrs)
33
+ return unless attrs
34
+ raise ArgumentError, "must be a Hash" if !attrs.is_a?(Hash)
35
+ write_attribute(:data, multi_json_dump(attrs.merge(data || {})))
36
+ end
37
+
38
+ def registration_ids=(ids)
39
+ ids = [ids] if ids && !ids.is_a?(Array)
40
+ super
41
+ end
42
+
43
+ def data
44
+ multi_json_load(read_attribute(:data)) if read_attribute(:data)
45
+ end
46
+
47
+ def payload
48
+ multi_json_dump(as_json)
49
+ end
50
+
51
+ def payload_size
52
+ payload.bytesize
53
+ end
54
+
55
+ def payload_data_size
56
+ multi_json_dump(as_json['data']).bytesize
57
+ end
58
+
59
+ class << self
60
+ def created_before(dt)
61
+ where("created_at < ?", dt)
62
+ end
63
+
64
+ def completed_and_older_than(dt)
65
+ completed.created_before(dt)
66
+ end
67
+ end
68
+ end
69
+ end