twilio-rails 1.0.0 → 1.1.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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +23 -17
  3. data/app/controllers/twilio/rails/phone_controller.rb +6 -2
  4. data/app/controllers/twilio/rails/sms_controller.rb +6 -5
  5. data/app/jobs/twilio/rails/phone/attach_recording_job.rb +1 -0
  6. data/app/jobs/twilio/rails/phone/finished_call_job.rb +1 -0
  7. data/app/jobs/twilio/rails/phone/unanswered_call_job.rb +1 -0
  8. data/app/operations/twilio/rails/application_operation.rb +2 -1
  9. data/app/operations/twilio/rails/find_or_create_phone_caller_operation.rb +2 -1
  10. data/app/operations/twilio/rails/phone/attach_recording_operation.rb +3 -2
  11. data/app/operations/twilio/rails/phone/base_operation.rb +1 -0
  12. data/app/operations/twilio/rails/phone/create_operation.rb +11 -8
  13. data/app/operations/twilio/rails/phone/find_operation.rb +1 -0
  14. data/app/operations/twilio/rails/phone/finished_call_operation.rb +2 -1
  15. data/app/operations/twilio/rails/phone/receive_recording_operation.rb +3 -2
  16. data/app/operations/twilio/rails/phone/start_call_operation.rb +15 -13
  17. data/app/operations/twilio/rails/phone/twiml/after_operation.rb +2 -1
  18. data/app/operations/twilio/rails/phone/twiml/base_operation.rb +11 -5
  19. data/app/operations/twilio/rails/phone/twiml/error_operation.rb +2 -1
  20. data/app/operations/twilio/rails/phone/twiml/greeting_operation.rb +3 -2
  21. data/app/operations/twilio/rails/phone/twiml/invalid_phone_number_operation.rb +25 -0
  22. data/app/operations/twilio/rails/phone/twiml/prompt_operation.rb +6 -6
  23. data/app/operations/twilio/rails/phone/twiml/prompt_response_operation.rb +2 -1
  24. data/app/operations/twilio/rails/phone/twiml/request_validation_failure_operation.rb +1 -0
  25. data/app/operations/twilio/rails/phone/twiml/timeout_operation.rb +5 -4
  26. data/app/operations/twilio/rails/phone/unanswered_call_operation.rb +2 -1
  27. data/app/operations/twilio/rails/phone/update_operation.rb +1 -0
  28. data/app/operations/twilio/rails/phone/update_response_operation.rb +1 -0
  29. data/app/operations/twilio/rails/sms/base_operation.rb +1 -0
  30. data/app/operations/twilio/rails/sms/create_operation.rb +2 -1
  31. data/app/operations/twilio/rails/sms/find_message_operation.rb +1 -0
  32. data/app/operations/twilio/rails/sms/find_operation.rb +1 -0
  33. data/app/operations/twilio/rails/sms/send_operation.rb +12 -12
  34. data/app/operations/twilio/rails/sms/twiml/base_operation.rb +1 -0
  35. data/app/operations/twilio/rails/sms/twiml/error_operation.rb +1 -0
  36. data/app/operations/twilio/rails/sms/twiml/message_operation.rb +4 -3
  37. data/app/operations/twilio/rails/sms/update_message_operation.rb +1 -0
  38. data/lib/generators/twilio/rails/install/install_generator.rb +1 -0
  39. data/lib/generators/twilio/rails/install/templates/initializer.rb +5 -8
  40. data/lib/generators/twilio/rails/install/templates/message.rb +1 -0
  41. data/lib/generators/twilio/rails/install/templates/phone_call.rb +1 -0
  42. data/lib/generators/twilio/rails/install/templates/phone_caller.rb +1 -0
  43. data/lib/generators/twilio/rails/install/templates/recording.rb +1 -0
  44. data/lib/generators/twilio/rails/install/templates/response.rb +1 -0
  45. data/lib/generators/twilio/rails/install/templates/sms_conversation.rb +1 -0
  46. data/lib/generators/twilio/rails/phone_tree/phone_tree_generator.rb +1 -0
  47. data/lib/generators/twilio/rails/sms_responder/sms_responder_generator.rb +1 -0
  48. data/lib/generators/twilio/rails/sms_responder/templates/responder.rb.erb +2 -2
  49. data/lib/tasks/rails_tasks.rake +6 -5
  50. data/lib/twilio/rails/client.rb +9 -8
  51. data/lib/twilio/rails/concerns/has_direction.rb +2 -1
  52. data/lib/twilio/rails/concerns/has_phone_number.rb +13 -4
  53. data/lib/twilio/rails/concerns/has_time_scopes.rb +1 -0
  54. data/lib/twilio/rails/configuration.rb +46 -42
  55. data/lib/twilio/rails/formatter.rb +35 -29
  56. data/lib/twilio/rails/models/message.rb +1 -0
  57. data/lib/twilio/rails/models/phone_call.rb +1 -0
  58. data/lib/twilio/rails/models/phone_caller.rb +4 -3
  59. data/lib/twilio/rails/models/recording.rb +1 -0
  60. data/lib/twilio/rails/models/response.rb +7 -6
  61. data/lib/twilio/rails/models/sms_conversation.rb +1 -0
  62. data/lib/twilio/rails/phone/base_tree.rb +8 -8
  63. data/lib/twilio/rails/phone/tree.rb +8 -8
  64. data/lib/twilio/rails/phone/tree_macros.rb +27 -8
  65. data/lib/twilio/rails/phone.rb +3 -2
  66. data/lib/twilio/rails/phone_number.rb +6 -5
  67. data/lib/twilio/rails/phone_number_formatter/north_america.rb +52 -0
  68. data/lib/twilio/rails/phone_number_formatter.rb +20 -0
  69. data/lib/twilio/rails/railtie.rb +6 -1
  70. data/lib/twilio/rails/sms/delegated_responder.rb +6 -5
  71. data/lib/twilio/rails/sms/responder.rb +3 -2
  72. data/lib/twilio/rails/sms.rb +3 -2
  73. data/lib/twilio/rails/version.rb +1 -1
  74. data/lib/twilio/rails.rb +12 -26
  75. metadata +22 -6
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Twilio
3
4
  module Rails
4
5
  class Configuration
5
6
  # Raised in initialization if the configuration is invalid.
6
- class Error < StandardError ; end
7
+ class Error < StandardError; end
7
8
 
8
9
  def initialize
9
10
  @finalized = false
@@ -14,10 +15,9 @@ module Twilio
14
15
  @account_sid = nil
15
16
  @auth_token = nil
16
17
  @spam_filter = nil
17
- @exception_notifier = nil
18
18
  @attach_recordings = true
19
- @yes_responses = [ "yes", "accept", "ya", "yeah", "true", "ok", "okay" ]
20
- @no_responses = [ "no", "naw", "nah", "reject", "decline", "negative", "not", "false" ]
19
+ @yes_responses = ["yes", "accept", "ya", "yeah", "true", "ok", "okay", "yep", "yup", "yes please"]
20
+ @no_responses = ["no", "naw", "nah", "reject", "decline", "negative", "not", "false", "nope", "no thank you", "know"]
21
21
  @message_class_name = "Message"
22
22
  @message_class = nil
23
23
  @phone_call_class_name = "PhoneCall"
@@ -33,12 +33,11 @@ module Twilio
33
33
  @phone_trees = PhoneTreeRegistry.new
34
34
  @sms_responders = SMSResponderRegistry.new
35
35
  @host = if ::Rails.configuration&.action_controller&.default_url_options
36
- "#{ ::Rails.configuration.action_controller.default_url_options[:protocol] }://#{ ::Rails.configuration.action_controller.default_url_options[:host] }"
37
- else
38
- nil
36
+ "#{::Rails.configuration.action_controller.default_url_options[:protocol]}://#{::Rails.configuration.action_controller.default_url_options[:host]}"
39
37
  end
40
38
  @controller_http_methods = [:get, :post]
41
39
  @include_phone_macros = []
40
+ @phone_number_formatter = Twilio::Rails::PhoneNumberFormatter::NorthAmerica.new
42
41
  end
43
42
 
44
43
  # This is the phone number that will be used to send SMS messages or start Phone Calls. It must be first configured
@@ -75,14 +74,6 @@ module Twilio
75
74
  # @return [Proc] a proc that will be called to filter messages, or `nil` if no filter is set.
76
75
  attr_accessor :spam_filter
77
76
 
78
- # A proc that will be called when an exception is raised in certain key points in the framework. This will never
79
- # capture the exception, it will raise regardless, but it is a good spot to send an email or notify in chat
80
- # if desired. The proc needs to accept `(exception, message, context, exception_binding)` as arguments. The
81
- # default is `nil`, which means no action will be taken.
82
- #
83
- # @return [Proc] a proc that will be called when an exception is raised in certain key points in the framework.
84
- attr_accessor :exception_notifier
85
-
86
77
  # Controls if recordings will be downloaded and attached to the `Recording` model in an ActiveStorage attachment.
87
78
  # This is `true` by default, but can be set to `false` to disable all downloads. It can also be set to a `Proc` or
88
79
  # callable that will receive the `Recording` instance and return a boolean for this specific instance. A typical
@@ -95,12 +86,14 @@ module Twilio
95
86
  # @return [true, false, Proc] a boolean or a proc that will be called to return a boolean to determine if reordings will be downloaded.
96
87
  attr_accessor :attach_recordings
97
88
 
98
- # A list of strings to be interpreted as yes or acceptance to a question.
89
+ # A list of strings to be interpreted as yes or acceptance to a question. Pairs with the
90
+ # {Twilio::Rails::Phone::TreeMacros#answer_yes?} method.
99
91
  #
100
92
  # @return [Array<String>] a list of strings to be interpreted as yes or acceptance to a question.
101
93
  attr_accessor :yes_responses
102
94
 
103
- # A list of strings to be interpreted as no or rejection to a question.
95
+ # A list of strings to be interpreted as no or rejection to a question. Pairs with the
96
+ # {Twilio::Rails::Phone::TreeMacros#answer_no?} method.
104
97
  #
105
98
  # @return [Array<String>] a list of strings to be interpreted as no or rejection to a question.
106
99
  attr_accessor :no_responses
@@ -153,8 +146,7 @@ module Twilio
153
146
  def host_domain
154
147
  return nil unless host.present?
155
148
  value = host.gsub(/\Ahttps?:\/\//, "")
156
- value = value.gsub(/:\d+\z/, "")
157
- value
149
+ value.gsub(/:\d+\z/, "")
158
150
  end
159
151
 
160
152
  # The HTTP methods that Twilio will use to call into the app. Defaults to `[:get, :post]` but can be restricted
@@ -193,6 +185,19 @@ module Twilio
193
185
  end
194
186
  end
195
187
 
188
+ # An instance of the class which will validate and format phone numbers. This is used internally to decide what
189
+ # phone numbers are valid, how to format them, how to parse them, how to display them, and what country assumptions
190
+ # to make.
191
+ #
192
+ # This class must implement four methods:
193
+ # * coerce(string)
194
+ # * valid?(string)
195
+ # * to_param(string)
196
+ # * display(string)
197
+ #
198
+ # @return [Object]
199
+ attr_accessor :phone_number_formatter
200
+
196
201
  # Flags that the configuration has been setup and should be validated and finalized.
197
202
  # If this is not called, the framework will not work, but the Railtie will not prevent
198
203
  # the application from starting.
@@ -215,12 +220,12 @@ module Twilio
215
220
  raise Error, "`auth_token` must be set" if @auth_token.blank?
216
221
  raise Error, "`logger` must be set" if @logger.blank?
217
222
  raise Error, "`spam_filter` must be callable" if @spam_filter && !@spam_filter.respond_to?(:call)
218
- raise Error, "`exception_notifier` must be callable" if @exception_notifier && !@exception_notifier.respond_to?(:call)
219
- raise Error, '`yes_responses` must be an array' unless @yes_responses.is_a?(Array)
220
- raise Error, '`no_responses` must be an array' unless @no_responses.is_a?(Array)
221
- raise Error, "`host` #{ @host.inspect } is not a valid URL of the format https://example.com without the trailing slash" unless @host =~ /\Ahttps?:\/\/[a-z0-9\-\.:]+\Z/i
222
- raise Error, "`controller_http_methods` must be an array containing one or both of `:get` and `:post` but was #{ @controller_http_methods.inspect }" unless @controller_http_methods.is_a?(Array) && @controller_http_methods.sort == [:get, :post].sort || @controller_http_methods == [:get] || @controller_http_methods == [:post]
223
- raise Error, "`include_phone_macros` must be a module, but received #{ @include_phone_macros.inspect }" unless @include_phone_macros.all? { |mod| mod.is_a?(Module) }
223
+ raise Error, "`yes_responses` must be an array" unless @yes_responses.is_a?(Array)
224
+ raise Error, "`no_responses` must be an array" unless @no_responses.is_a?(Array)
225
+ raise Error, "`host` #{@host.inspect} is not a valid URL of the format https://example.com without the trailing slash" unless /\Ahttps?:\/\/[a-z0-9\-\.:]+\Z/i.match?(@host)
226
+ raise Error, "`controller_http_methods` must be an array containing one or both of `:get` and `:post` but was #{@controller_http_methods.inspect}" unless [[:get], [:post], [:get, :post], [:post, :get]].any? { |v| @controller_http_methods == v }
227
+ raise Error, "`include_phone_macros` must be a module, but received #{@include_phone_macros.inspect}" unless @include_phone_macros.all? { |mod| mod.is_a?(Module) }
228
+ raise Error, "`phone_number_formatter` must be set" unless @phone_number_formatter
224
229
  nil
225
230
  end
226
231
 
@@ -239,13 +244,13 @@ module Twilio
239
244
  :response_class_name,
240
245
  :sms_conversation_class_name,
241
246
  :message_class_name,
242
- :recording_class_name,
247
+ :recording_class_name
243
248
  ].each do |attribute|
244
- value = self.send(attribute)
249
+ value = send(attribute)
245
250
  raise Error, "`#{attribute}` must be set to a string name" if value.blank? || !value.is_a?(String)
246
251
  begin
247
252
  klass = value.constantize
248
- instance_variable_set("@#{ attribute.to_s.gsub("_name", "") }", klass)
253
+ instance_variable_set("@#{attribute.to_s.gsub("_name", "")}", klass)
249
254
  rescue NameError
250
255
  raise Error, "`#{attribute}` must be a valid class name but could not be found or constantized"
251
256
  end
@@ -288,7 +293,7 @@ module Twilio
288
293
  # @yield [nil] if a block is passed, it will be called and the result will be used as the value.
289
294
  # @yieldreturn [Class, String, Proc] containing the Class to be lazily initialized when {#finalize!} is called.
290
295
  # @return [nil]
291
- def register(klass_or_proc=nil, &block)
296
+ def register(klass_or_proc = nil, &block)
292
297
  raise Error, "Must pass either a param or a block" unless klass_or_proc.present? ^ block.present?
293
298
  value = klass_or_proc || block
294
299
 
@@ -303,7 +308,7 @@ module Twilio
303
308
  # @param [String, Symbol] name of the phone tree or SMS responder to find.
304
309
  # @return [Class] the phone tree or SMS responder class.
305
310
  def for(name)
306
- @registry[name.to_s] || raise(error_class, "No responder registered for '#{ name }'")
311
+ @registry[name.to_s] || raise(error_class, "Name '#{name}' has not been registered and cannot be found.")
307
312
  end
308
313
 
309
314
  # Returns all the phone trees or SMS responders as a read-only hash, keyed by name.
@@ -316,7 +321,7 @@ module Twilio
316
321
  private
317
322
 
318
323
  def add_to_registry(value)
319
- raise NotImplementedError
324
+ raise NoMethodError
320
325
  end
321
326
 
322
327
  def error_class
@@ -333,14 +338,14 @@ module Twilio
333
338
  value = value.call if value.respond_to?(:call)
334
339
  begin
335
340
  value = value.constantize if value.is_a?(String)
336
- rescue NameError => e
337
- raise(error_class, "Responder class '#{ value }' could not be constantized")
341
+ rescue NameError
342
+ raise(error_class, "Responder class '#{value}' could not be constantized")
338
343
  end
339
344
  raise(error_class, "Responder cannot be blank") unless value.present?
340
- raise(error_class, "Responder must be a class but got #{ value.inspect }") unless value.is_a?(Class)
345
+ raise(error_class, "Responder must be a class but got #{value.inspect}") unless value.is_a?(Class)
341
346
  name = value.responder_name
342
347
  raise(error_class, "Responder name cannot be blank") unless name.present?
343
- raise(error_class, "Responder name '#{ name }' is already registered") if @registry[name]
348
+ raise(error_class, "Responder name '#{name}' is already registered") if @registry[name]
344
349
  @registry[name] = value
345
350
  end
346
351
 
@@ -358,16 +363,15 @@ module Twilio
358
363
  value = value.call if value.respond_to?(:call)
359
364
  begin
360
365
  value = value.constantize if value.is_a?(String)
361
- rescue NameError => e
362
- raise(error_class, "Tree class '#{ value }' could not be constantized")
366
+ rescue NameError
367
+ raise(error_class, "Tree class '#{value}' could not be constantized")
363
368
  end
364
- raise(error_class, "Tree cannot be blank #{ value }") unless value.present?
365
- raise(error_class, "Tree is not a Twilio::Rails::Phone::BaseTree class #{ value }") unless value.is_a?(Class)
366
- raise(error_class, "Tree is not a Twilio::Rails::Phone::BaseTree #{ value }") unless value.ancestors.include?(Twilio::Rails::Phone::BaseTree)
369
+ raise(error_class, "Tree cannot be blank #{value}") unless value.present?
370
+ raise(error_class, "Tree is not a Twilio::Rails::Phone::BaseTree class #{value}") unless value.is_a?(Class)
371
+ raise(error_class, "Tree is not a Twilio::Rails::Phone::BaseTree #{value}") unless value.ancestors.include?(Twilio::Rails::Phone::BaseTree)
367
372
  name = value.tree_name
368
373
  raise(error_class, "Tree name cannot be blank") unless name.present?
369
- raise(error_class, "Tree name '#{ name }' is already registered") if @registry[name]
370
- klass = klass.constantize if klass.is_a?(String)
374
+ raise(error_class, "Tree name '#{name}' is already registered") if @registry[name]
371
375
  @registry[name] = value.tree
372
376
  end
373
377
 
@@ -1,12 +1,10 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Twilio
3
4
  module Rails
4
5
  module Formatter
5
6
  extend self
6
7
 
7
- PHONE_NUMBER_REGEX = /\A\+1[0-9]{10}\Z/
8
- PHONE_NUMBER_SEGMENTS_REGEX = /\A\+1([0-9]{3})([0-9]{3})([0-9]{4})\Z/
9
-
10
8
  # Takes in a string or a {Twilio::Rails::PhoneNumber} or something that responds to `to_s` and turns it into a
11
9
  # consistently formatted valid north american 10 digit phone number prefixed with 1 and plus. It uses the format
12
10
  # Twilio expects which is "+15555555555" or returns `nil` if it cannot be coerced.
@@ -14,17 +12,13 @@ module Twilio
14
12
  # @param string [String, Twilio::Rails::PhoneNumber, nil, Object] the input to turn into a phone number string.
15
13
  # @return [String, nil] the phone number string or nil.
16
14
  def coerce_to_valid_phone_number(string)
17
- string = string.number if string.is_a?(Twilio::Rails::PhoneNumber)
18
- string = string.to_s.presence
19
-
20
- if string
21
- string = string.gsub(/[^0-9]/, "")
22
- string = "1#{ string }" unless string.starts_with?("1")
23
- string = "+#{ string }"
24
- string = nil unless valid_north_american_phone_number?(string)
25
- end
15
+ Twilio::Rails.deprecator.warn(<<~DEPRECATION.strip)
16
+ Twilio::Rails::Formatter#coerce_to_valid_phone_number(s) is deprecated and will be removed in the next major version.
26
17
 
27
- string
18
+ Set Twilio::Rails.config.phone_number_formatter = Twilio::Rails::PhoneNumberFormatter::NorthAmerica.new
19
+ and use Twilio::Rails.config.phone_number_formatter.coerce(s) instead.
20
+ DEPRECATION
21
+ north_america_formatter.coerce(string)
28
22
  end
29
23
 
30
24
  # Takes in a string or a {Twilio::Rails::PhoneNumber} or something that responds to `to_s` and validates it
@@ -33,8 +27,13 @@ module Twilio
33
27
  # @param phone_number [String, Twilio::Rails::PhoneNumber, nil] the input to validate as a phone number.
34
28
  # @return [true, false]
35
29
  def valid_north_american_phone_number?(phone_number)
36
- phone_number = phone_number.number if phone_number.is_a?(Twilio::Rails::PhoneNumber)
37
- !!phone_number&.match?(PHONE_NUMBER_REGEX)
30
+ Twilio::Rails.deprecator.warn(<<~DEPRECATION.strip)
31
+ Twilio::Rails::Formatter#valid_north_american_phone_number?(s) is deprecated and will be removed in the next major version.
32
+
33
+ Set Twilio::Rails.config.phone_number_formatter = Twilio::Rails::PhoneNumberFormatter::NorthAmerica.new
34
+ and use Twilio::Rails.config.phone_number_formatter.valid?(s) instead.
35
+ DEPRECATION
36
+ north_america_formatter.valid?(phone_number)
38
37
  end
39
38
 
40
39
  # Takes in a string or a {Twilio::Rails::PhoneNumber} or something that responds to `to_s` and turns it into
@@ -44,11 +43,13 @@ module Twilio
44
43
  # @param phone_number [String, Twilio::Rails::PhoneNumber, nil] the input to turn into a phone number string.
45
44
  # @return [String] the phone number string or empty string if invalid.
46
45
  def to_phone_number_url_param(phone_number)
47
- phone_number = coerce_to_valid_phone_number(phone_number)
48
- return "" unless phone_number
49
- matches = phone_number.match(PHONE_NUMBER_SEGMENTS_REGEX)
50
- raise Twilio::Rails::Error, "[to_phone_number_url_param] Phone number marked as valid but could not capture. I made a bad regex: #{ phone_number }" unless matches
51
- matches.captures.join("-")
46
+ Twilio::Rails.deprecator.warn(<<~DEPRECATION.strip)
47
+ Twilio::Rails::Formatter#to_phone_number_url_param(s) is deprecated and will be removed in the next major version.
48
+
49
+ Set Twilio::Rails.config.phone_number_formatter = Twilio::Rails::PhoneNumberFormatter::NorthAmerica.new
50
+ and use Twilio::Rails.config.phone_number_formatter.to_param(s) instead.
51
+ DEPRECATION
52
+ north_america_formatter.to_param(phone_number)
52
53
  end
53
54
 
54
55
  # Takes in a string or a {Twilio::Rails::PhoneNumber} or something that responds to `to_s` and turns it into a
@@ -58,14 +59,13 @@ module Twilio
58
59
  # @param phone_number [String, Twilio::Rails::PhoneNumber, nil] the input to turn into a phone number string.
59
60
  # @return [String, Object] the phone number string or the original object if invalid.
60
61
  def display_phone_number(phone_number)
61
- coerced_phone_number = coerce_to_valid_phone_number(phone_number)
62
- if coerced_phone_number
63
- matches = coerced_phone_number.match(PHONE_NUMBER_SEGMENTS_REGEX)
64
- raise Twilio::Rails::Error, "[display_phone_number] Phone number marked as valid but could not capture. I made a bad regex: #{ phone_number }" unless matches
65
- "(#{ matches.captures[0] }) #{ matches.captures[1] } #{ matches.captures[2] }"
66
- else
67
- phone_number
68
- end
62
+ Twilio::Rails.deprecator.warn(<<~DEPRECATION.strip)
63
+ Twilio::Rails::Formatter#display_phone_number(s) is deprecated and will be removed in the next major version.
64
+
65
+ Set Twilio::Rails.config.phone_number_formatter = Twilio::Rails::PhoneNumberFormatter::NorthAmerica.new
66
+ and use Twilio::Rails.config.phone_number_formatter.display(s) instead.
67
+ DEPRECATION
68
+ north_america_formatter.display(phone_number)
69
69
  end
70
70
 
71
71
  # Formats a city, province, and country into a single string, correctly handling blanks, and formatting countries.
@@ -85,9 +85,15 @@ module Twilio
85
85
  [
86
86
  city.presence&.titleize,
87
87
  province,
88
- country_name,
88
+ country_name
89
89
  ].reject(&:blank?).join(", ")
90
90
  end
91
+
92
+ private
93
+
94
+ def north_america_formatter
95
+ @north_america_formatter ||= Twilio::Rails::PhoneNumberFormatter::NorthAmerica.new
96
+ end
91
97
  end
92
98
  end
93
99
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Twilio
3
4
  module Rails
4
5
  module Models
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Twilio
3
4
  module Rails
4
5
  module Models
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Twilio
3
4
  module Rails
4
5
  module Models
@@ -21,7 +22,7 @@ module Twilio
21
22
  # @param phone_number_string [String, Twilio::Rails::PhoneNumber] The phone number to find the record.
22
23
  # @return [Twilio::Rails::Models::PhoneCaller, nil] The phone caller record or `nil` if not found.
23
24
  def for(phone_number_string)
24
- phone_number = Twilio::Rails::Formatter.coerce_to_valid_phone_number(phone_number_string)
25
+ phone_number = Twilio::Rails::PhoneNumberFormatter.coerce(phone_number_string)
25
26
  find_by(phone_number: phone_number) if phone_number.present?
26
27
  end
27
28
  end
@@ -45,11 +46,11 @@ module Twilio
45
46
 
46
47
  # @return [Array<Twilio::Rails::Models::SmsConversation>] All SMS conversations for the phone caller.
47
48
  def sms_conversations
48
- Twilio::Rails.config.sms_conversation_class.phone_number(self.phone_number)
49
+ Twilio::Rails.config.sms_conversation_class.phone_number(phone_number)
49
50
  end
50
51
 
51
52
  # Returns the digits as a `String` as entered through the keypad during a phone call as `gather:`. Returns
52
- #`nil` if the response is not found, if the response has no digits, or if the response was a timeout. Can
53
+ # `nil` if the response is not found, if the response has no digits, or if the response was a timeout. Can
53
54
  # include both `*` and `#` characters if the caller pressed them.
54
55
  #
55
56
  # @param prompt [String, Symbol] The prompt handle to query.
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Twilio
3
4
  module Rails
4
5
  module Models
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Twilio
3
4
  module Rails
4
5
  module Models
@@ -19,11 +20,11 @@ module Twilio
19
20
  delegate :phone_caller, to: :phone_call
20
21
 
21
22
  scope :completed, -> { where(timeout: false) }
22
- scope :recent_transcriptions, ->(number=5) { completed.order(created_at: :desc).where.not(transcription: nil).limit(number) }
23
+ scope :recent_transcriptions, ->(number = 5) { completed.order(created_at: :desc).where.not(transcription: nil).limit(number) }
23
24
  scope :final_timeout_check, ->(count:, prompt_handle:) {
24
25
  prompt(prompt_handle).order(created_at: :desc).limit(count)
25
26
  }
26
- scope :tree, ->(name) { joins(:phone_call).where(phone_calls: { tree_name: name }) }
27
+ scope :tree, ->(name) { joins(:phone_call).where(phone_calls: {tree_name: name}) }
27
28
  scope :prompt, ->(prompt_handle) { where(prompt_handle: prompt_handle) }
28
29
  scope :in_order, -> { reorder(created_at: :asc) }
29
30
  scope :transcribed, -> { where(transcribed: true) }
@@ -37,9 +38,9 @@ module Twilio
37
38
  # @param prompt [String, Symbol, Array] The prompt handle or an array of them.
38
39
  # @return [true, false] true if the response is for the given prompt and tree.
39
40
  def is?(tree:, prompt:)
40
- trees = Array(tree).map { |t| t.is_a?(Twilio::Rails::Phone::Tree) ? t.name : t.to_s }
41
+ Array(tree).map { |t| t.is_a?(Twilio::Rails::Phone::Tree) ? t.name : t.to_s }
41
42
 
42
- from?(tree: tree) && Array(prompt).map(&:to_s).reject(&:blank?).include?(self.prompt_handle)
43
+ from?(tree: tree) && Array(prompt).map(&:to_s).reject(&:blank?).include?(prompt_handle)
43
44
  end
44
45
 
45
46
  # Checks if the response is for a given tree or trees or tree names.
@@ -49,7 +50,7 @@ module Twilio
49
50
  def from?(tree:)
50
51
  trees = Array(tree).map { |t| t.is_a?(Twilio::Rails::Phone::Tree) ? t.name : t.to_s }
51
52
 
52
- trees.include?(self.phone_call.tree_name)
53
+ trees.include?(phone_call.tree_name)
53
54
  end
54
55
 
55
56
  # Returns the digits as an `Integer` entered through the keypad during a phone call as `gather:`. Returns `nil`
@@ -59,7 +60,7 @@ module Twilio
59
60
  # @return [Integer, nil] The digits as entered by the caller or `nil` if not found or not present.
60
61
  def integer_digits
61
62
  return nil unless digits.present?
62
- return nil unless digits =~ /\A[0-9]+\Z/
63
+ return nil unless /\A[0-9]+\Z/.match?(digits)
63
64
  digits.to_i
64
65
  end
65
66
 
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Twilio
3
4
  module Rails
4
5
  module Models
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Twilio
3
4
  module Rails
4
5
  module Phone
@@ -85,7 +86,7 @@ module Twilio
85
86
  # @param message [String, Hash, Array, Proc] The message to play to the caller.
86
87
  # @param prompt [Symbol, Hash, Proc] The name of the next prompt.
87
88
  # @return [nil]
88
- def greeting(message: nil, prompt:)
89
+ def greeting(prompt:, message: nil)
89
90
  tree.greeting = Twilio::Rails::Phone::Tree::After.new(message: message, prompt: prompt)
90
91
  nil
91
92
  end
@@ -122,7 +123,6 @@ module Twilio
122
123
  # response instance. However, this all happens asynchronously with no guarantee of time or success. Voice
123
124
  # accepts the following configuration keys:
124
125
  # * `:length`: The number of seconds to record. The default is 10.
125
- # * `:beep`: A boolean if the gather is preceeded by a beep. The default is `true`.
126
126
  # * `:transcribe`: A boolean if Twilio should attempt to transcribe the audio and send it back as text. The
127
127
  # framework handles this all asynchronously and will update the `transcription` field. Default is `false`.
128
128
  # * `:profanity_filter`: Replaces any profanity in the transcription with ***. Default is `false`.
@@ -154,7 +154,7 @@ module Twilio
154
154
  # * `Proc`: A proc that will be called after the message and gather have been called. The proc will receive
155
155
  # the current {Twilio::Rails::Models::Response} instance as an argument. The proc must return one of the
156
156
  # above.
157
- def prompt(prompt_name, message: nil, gather: nil, after:)
157
+ def prompt(prompt_name, after:, message: nil, gather: nil)
158
158
  tree.prompts[prompt_name] = Twilio::Rails::Phone::Tree::Prompt.new(name: prompt_name, message: message, gather: gather, after: after)
159
159
  nil
160
160
  end
@@ -185,10 +185,10 @@ module Twilio
185
185
  nil
186
186
  end
187
187
 
188
- # The `message:` object that played to the caller if a call from an invalid phone number is received. The
189
- # important case here is a number from outside of North America. This is currently a limitation of the
190
- # framework. The default is `nil` and no action is taken. See the documentation for {.prompt} for what a
191
- # message object can contain.
188
+ # The `message:` object that played to the caller if a call from an invalid phone number is received. This can
189
+ # be an empty or "Unknown" number, but the important case here is a number from outside of North America. This
190
+ # is currently a limitation of theframework. The default is `nil` and no action is taken. See the
191
+ # documentation for {.prompt} for what a message object can contain.
192
192
  #
193
193
  # @param message [String, Hash, Array, Proc] The message to play to the caller.
194
194
  def invalid_phone_number(message)
@@ -201,7 +201,7 @@ module Twilio
201
201
  #
202
202
  # @return [String] the name of the tree.
203
203
  def tree_name
204
- self.name.demodulize.underscore.sub(/_tree\z/, "")
204
+ name.demodulize.underscore.sub(/_tree\z/, "")
205
205
  end
206
206
 
207
207
  # The instance of {Twilio::Rails::Phone::Tree} built from the DSL. Should be treated as read-only. Used
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Twilio
3
4
  module Rails
4
5
  module Phone
@@ -24,14 +25,14 @@ module Twilio
24
25
  #
25
26
  # @return [String] The outbound URL for the phone tree.
26
27
  def outbound_url
27
- "#{ ::Twilio::Rails.config.host }#{ ::Twilio::Rails::Engine.routes.url_helpers.phone_outbound_path(tree_name: name, format: :xml) }"
28
+ "#{::Twilio::Rails.config.host}#{::Twilio::Rails::Engine.routes.url_helpers.phone_outbound_path(tree_name: name, format: :xml)}"
28
29
  end
29
30
 
30
31
  # The fully qualified URL for the tree used by Twilio to be configured in the dashboard.
31
32
  #
32
33
  # @return [String] The inbound URL for the phone tree.
33
34
  def inbound_url
34
- "#{ ::Twilio::Rails.config.host }#{ ::Twilio::Rails::Engine.routes.url_helpers.phone_inbound_path(tree_name: name, format: :xml) }"
35
+ "#{::Twilio::Rails.config.host}#{::Twilio::Rails::Engine.routes.url_helpers.phone_inbound_path(tree_name: name, format: :xml)}"
35
36
  end
36
37
 
37
38
  class Prompt
@@ -102,7 +103,6 @@ module Twilio
102
103
  @args[:number] ||= 1
103
104
  elsif voice?
104
105
  @args[:length] ||= 10
105
- @args[:beep] = true unless @args.key?(:beep)
106
106
  @args[:transcribe] = false unless @args.key?(:transcribe)
107
107
  @args[:profanity_filter] = false unless @args.key?(:profanity_filter)
108
108
  elsif speech?
@@ -137,7 +137,7 @@ module Twilio
137
137
  end
138
138
 
139
139
  class Message
140
- attr_reader :value, :voice, :block
140
+ attr_reader :voice, :block
141
141
 
142
142
  def initialize(say: nil, play: nil, pause: nil, voice: nil, &block)
143
143
  @say = say.presence
@@ -150,8 +150,8 @@ module Twilio
150
150
  raise Twilio::Rails::Phone::InvalidTreeError, "must only have one of say: play: pause:" if (@say && @play) || (@say && @pause) || (@play && @pause)
151
151
  raise Twilio::Rails::Phone::InvalidTreeError, "say: must be a string or proc" if @say && !(@say.is_a?(String) || @say.is_a?(Proc))
152
152
  raise Twilio::Rails::Phone::InvalidTreeError, "play: must be a string or proc" if @play && !(@play.is_a?(String) || @play.is_a?(Proc))
153
- raise Twilio::Rails::Phone::InvalidTreeError, "play: be a valid url but is #{ @play }" if @play && @play.is_a?(String) && !@play.match(/^https?:\/\/.+/)
154
- raise Twilio::Rails::Phone::InvalidTreeError, "pause: must be over zero but is #{ @pause }" if @pause && @pause <= 0
153
+ raise Twilio::Rails::Phone::InvalidTreeError, "play: be a valid url but is #{@play}" if @play&.is_a?(String) && !@play.match(/^https?:\/\/.+/)
154
+ raise Twilio::Rails::Phone::InvalidTreeError, "pause: must be over zero but is #{@pause}" if @pause && @pause <= 0
155
155
  raise Twilio::Rails::Phone::InvalidTreeError, "block is only valid for say:" if block_given? && (@play || @pause)
156
156
  end
157
157
 
@@ -184,7 +184,7 @@ module Twilio
184
184
  if set.is_a?(Hash)
185
185
  set = set.symbolize_keys
186
186
  if set.key?(:message)
187
- raise Twilio::Rails::Phone::InvalidTreeError, "MessageSet should never receive a hash with any key other than :message but received #{ set }" if set.keys != [:message]
187
+ raise Twilio::Rails::Phone::InvalidTreeError, "MessageSet should never receive a hash with any key other than :message but received #{set}" if set.keys != [:message]
188
188
  set = set[:message]
189
189
  end
190
190
  end
@@ -202,7 +202,7 @@ module Twilio
202
202
  elsif message.is_a?(Hash)
203
203
  @messages << Twilio::Rails::Phone::Tree::Message.new(**message.symbolize_keys)
204
204
  else
205
- raise Twilio::Rails::Phone::InvalidTreeError, "message value #{ message } is not valid"
205
+ raise Twilio::Rails::Phone::InvalidTreeError, "message value #{message} is not valid"
206
206
  end
207
207
  end
208
208
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Twilio
3
4
  module Rails
4
5
  module Phone
@@ -21,7 +22,7 @@ module Twilio
21
22
  timeout: timeout.to_i.presence || 6,
22
23
  number: 1,
23
24
  interrupt: true,
24
- finish_on_key: "",
25
+ finish_on_key: ""
25
26
  }
26
27
  end
27
28
 
@@ -34,16 +35,16 @@ module Twilio
34
35
  # @return [String] the digits joined with commas.
35
36
  def digits(num)
36
37
  return "" if num.blank?
37
- num.to_s.split("").join(", ")
38
+ num.to_s.chars.join(", ")
38
39
  end
39
40
 
40
41
  # Pause for a number of seconds, defaults to 1 second. Useful when putting space between segments of speech.
41
42
  #
42
43
  # @param seconds [Integer] the number of seconds to pause for, defaults to 1 second.
43
44
  # @return [Hash] formatted to pass to `message:`.
44
- def pause(seconds=nil)
45
+ def pause(seconds = nil)
45
46
  {
46
- pause: (seconds.presence || 1),
47
+ pause: seconds.presence || 1
47
48
  }
48
49
  end
49
50
 
@@ -66,7 +67,7 @@ module Twilio
66
67
  raise Twilio::Rails::Phone::Error, "`numbered_choices` macro got an empty array" if choices.empty?
67
68
  raise Twilio::Rails::Phone::Error, "`numbered_choices` macro cannot be more than 9" if choices.length > 9
68
69
  prefix ||= "For"
69
- choices.each_with_index.map { |choice, index| "#{ prefix } #{ choice }, press #{ index + 1 }." }.join(" ")
70
+ choices.each_with_index.map { |choice, index| "#{prefix} #{choice}, press #{index + 1}." }.join(" ")
70
71
  end
71
72
 
72
73
  # Validates if the response object includes a digit that is within the range of the choices array. This pairs
@@ -90,6 +91,15 @@ module Twilio
90
91
  Twilio::Rails.config.yes_responses
91
92
  end
92
93
 
94
+ # Matches if the entire passed in string is included in the {Twilio::Rails::Configuration#yes_responses} and is
95
+ # considered a "yes".
96
+ #
97
+ # @param filename [String] the string to match against.
98
+ # @return [true, false] if the passed in string matches.
99
+ def answer_yes?(string)
100
+ answers_yes.include?((string || "").downcase.strip.gsub(/[.,!?]/, ""))
101
+ end
102
+
93
103
  # The list of configured answers that are considered "no" from {Twilio::Rails::Configuration#no_responses}.
94
104
  #
95
105
  # @return [Array<String>] the list of configured answers that are considered "no".
@@ -97,6 +107,15 @@ module Twilio
97
107
  Twilio::Rails.config.no_responses
98
108
  end
99
109
 
110
+ # Matches if the entire passed in string is included in the {Twilio::Rails::Configuration#no_responses} and is
111
+ # considered a "no".
112
+ #
113
+ # @param filename [String] the string to match against.
114
+ # @return [true, false] if the passed in string matches.
115
+ def answer_no?(string)
116
+ answers_no.include?((string || "").downcase.strip.gsub(/[.,!?]/, ""))
117
+ end
118
+
100
119
  # Finds and validates the existence of a file in the `public` folder. Formats that link to include the
101
120
  # configured hose from {Twilio::Rails::Configuration#host}, and returns a fully qualified URL to the file. This
102
121
  # is useful for playing audio files in a `message:` block. If the file is not found
@@ -109,9 +128,9 @@ module Twilio
109
128
  local_path = ::Rails.public_path.join(filename)
110
129
 
111
130
  if File.exist?(local_path)
112
- "#{ ::Twilio::Rails.config.host }/#{ filename }"
131
+ "#{::Twilio::Rails.config.host}/#{filename}"
113
132
  else
114
- raise Twilio::Rails::Phone::Error, "Cannot find public file '#{ filename }' at #{ local_path }"
133
+ raise Twilio::Rails::Phone::Error, "Cannot find public file '#{filename}' at #{local_path}"
115
134
  end
116
135
  end
117
136
 
@@ -120,7 +139,7 @@ module Twilio
120
139
  # @param filename [String] the filename of the file to play located in the `public` folder.
121
140
  # @return [Hash] formatted to pass to `message:`.
122
141
  def play_public_file(filename)
123
- { play: public_file(filename) }
142
+ {play: public_file(filename)}
124
143
  end
125
144
 
126
145
  # Expose a {Twilio::TwiML::Say} node to be used in a `message:` block. This can be used to form Speech Synthesis