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,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