twilio-rails 1.0.1 → 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 +20 -14
  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 -5
  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 +42 -40
  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 -7
  63. data/lib/twilio/rails/phone/tree.rb +8 -7
  64. data/lib/twilio/rails/phone/tree_macros.rb +9 -9
  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 +20 -6
@@ -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
@@ -153,7 +154,7 @@ module Twilio
153
154
  # * `Proc`: A proc that will be called after the message and gather have been called. The proc will receive
154
155
  # the current {Twilio::Rails::Models::Response} instance as an argument. The proc must return one of the
155
156
  # above.
156
- def prompt(prompt_name, message: nil, gather: nil, after:)
157
+ def prompt(prompt_name, after:, message: nil, gather: nil)
157
158
  tree.prompts[prompt_name] = Twilio::Rails::Phone::Tree::Prompt.new(name: prompt_name, message: message, gather: gather, after: after)
158
159
  nil
159
160
  end
@@ -184,10 +185,10 @@ module Twilio
184
185
  nil
185
186
  end
186
187
 
187
- # The `message:` object that played to the caller if a call from an invalid phone number is received. The
188
- # important case here is a number from outside of North America. This is currently a limitation of the
189
- # framework. The default is `nil` and no action is taken. See the documentation for {.prompt} for what a
190
- # 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.
191
192
  #
192
193
  # @param message [String, Hash, Array, Proc] The message to play to the caller.
193
194
  def invalid_phone_number(message)
@@ -200,7 +201,7 @@ module Twilio
200
201
  #
201
202
  # @return [String] the name of the tree.
202
203
  def tree_name
203
- self.name.demodulize.underscore.sub(/_tree\z/, "")
204
+ name.demodulize.underscore.sub(/_tree\z/, "")
204
205
  end
205
206
 
206
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
@@ -136,7 +137,7 @@ module Twilio
136
137
  end
137
138
 
138
139
  class Message
139
- attr_reader :value, :voice, :block
140
+ attr_reader :voice, :block
140
141
 
141
142
  def initialize(say: nil, play: nil, pause: nil, voice: nil, &block)
142
143
  @say = say.presence
@@ -149,8 +150,8 @@ module Twilio
149
150
  raise Twilio::Rails::Phone::InvalidTreeError, "must only have one of say: play: pause:" if (@say && @play) || (@say && @pause) || (@play && @pause)
150
151
  raise Twilio::Rails::Phone::InvalidTreeError, "say: must be a string or proc" if @say && !(@say.is_a?(String) || @say.is_a?(Proc))
151
152
  raise Twilio::Rails::Phone::InvalidTreeError, "play: must be a string or proc" if @play && !(@play.is_a?(String) || @play.is_a?(Proc))
152
- raise Twilio::Rails::Phone::InvalidTreeError, "play: be a valid url but is #{ @play }" if @play && @play.is_a?(String) && !@play.match(/^https?:\/\/.+/)
153
- 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
154
155
  raise Twilio::Rails::Phone::InvalidTreeError, "block is only valid for say:" if block_given? && (@play || @pause)
155
156
  end
156
157
 
@@ -183,7 +184,7 @@ module Twilio
183
184
  if set.is_a?(Hash)
184
185
  set = set.symbolize_keys
185
186
  if set.key?(:message)
186
- 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]
187
188
  set = set[:message]
188
189
  end
189
190
  end
@@ -201,7 +202,7 @@ module Twilio
201
202
  elsif message.is_a?(Hash)
202
203
  @messages << Twilio::Rails::Phone::Tree::Message.new(**message.symbolize_keys)
203
204
  else
204
- raise Twilio::Rails::Phone::InvalidTreeError, "message value #{ message } is not valid"
205
+ raise Twilio::Rails::Phone::InvalidTreeError, "message value #{message} is not valid"
205
206
  end
206
207
  end
207
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
@@ -115,7 +116,6 @@ module Twilio
115
116
  answers_no.include?((string || "").downcase.strip.gsub(/[.,!?]/, ""))
116
117
  end
117
118
 
118
-
119
119
  # Finds and validates the existence of a file in the `public` folder. Formats that link to include the
120
120
  # configured hose from {Twilio::Rails::Configuration#host}, and returns a fully qualified URL to the file. This
121
121
  # is useful for playing audio files in a `message:` block. If the file is not found
@@ -128,9 +128,9 @@ module Twilio
128
128
  local_path = ::Rails.public_path.join(filename)
129
129
 
130
130
  if File.exist?(local_path)
131
- "#{ ::Twilio::Rails.config.host }/#{ filename }"
131
+ "#{::Twilio::Rails.config.host}/#{filename}"
132
132
  else
133
- 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}"
134
134
  end
135
135
  end
136
136
 
@@ -139,7 +139,7 @@ module Twilio
139
139
  # @param filename [String] the filename of the file to play located in the `public` folder.
140
140
  # @return [Hash] formatted to pass to `message:`.
141
141
  def play_public_file(filename)
142
- { play: public_file(filename) }
142
+ {play: public_file(filename)}
143
143
  end
144
144
 
145
145
  # Expose a {Twilio::TwiML::Say} node to be used in a `message:` block. This can be used to form Speech Synthesis
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Twilio
3
4
  module Rails
4
5
  module Phone
5
6
  # Base error class for errors relating to Twilio phone interactions.
6
- class Error < ::Twilio::Rails::Error ; end
7
+ class Error < ::Twilio::Rails::Error; end
7
8
 
8
9
  # Error raised when attempting to build a phone tree.
9
- class InvalidTreeError < Error ; end
10
+ class InvalidTreeError < Error; end
10
11
  end
11
12
  end
12
13
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Twilio
3
4
  module Rails
4
5
  # A phone number object that includes the country and some optional metadata.
@@ -10,8 +11,8 @@ module Twilio
10
11
  # @param label [String, nil] an optional label for the phone number, such as its source or purpose.
11
12
  # @param project [String, nil] an optional project identifier for grouping phone numbers.
12
13
  def initialize(number:, country:, label: nil, project: nil)
13
- @number = Twilio::Rails::Formatter.coerce_to_valid_phone_number(number)
14
- raise Twilio::Rails::Phone::Error, "Invalid phone number '#{ number }'" unless @number
14
+ @number = Twilio::Rails::PhoneNumberFormatter.coerce(number)
15
+ raise Twilio::Rails::Phone::Error, "Invalid phone number '#{number}'" unless @number
15
16
  @country = country&.upcase
16
17
  @label = label
17
18
  @project = project.presence&.to_s
@@ -19,9 +20,9 @@ module Twilio
19
20
 
20
21
  # @return [String] a human readable string representation of the phone number and its metadata.
21
22
  def to_s
22
- s = "Phone number #{ number } (#{ country })"
23
- s = "#{ s } #{ label }" if label.present?
24
- s = "#{ s } for #{ project }" if project.present?
23
+ s = "Phone number #{number} (#{country})"
24
+ s = "#{s} #{label}" if label.present?
25
+ s = "#{s} for #{project}" if project.present?
25
26
  s
26
27
  end
27
28
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twilio
4
+ module Rails
5
+ module PhoneNumberFormatter
6
+ # Formats phone numbers as North American 10 digit numbers only, and treats any other number as invalid.
7
+ # This is the legacy behavior from 1.0 which will be the default still in 1.1 as an upgrade path.
8
+ class NorthAmerica
9
+ PHONE_NUMBER_REGEX = /\A\+1[0-9]{10}\Z/
10
+ PHONE_NUMBER_SEGMENTS_REGEX = /\A\+1([0-9]{3})([0-9]{3})([0-9]{4})\Z/
11
+
12
+ def coerce(string)
13
+ string = string.number if string.is_a?(Twilio::Rails::PhoneNumber)
14
+ string = string.to_s.presence
15
+
16
+ if string
17
+ string = string.gsub(/[^0-9]/, "")
18
+ string = "1#{string}" unless string.starts_with?("1")
19
+ string = "+#{string}"
20
+ string = nil unless valid?(string)
21
+ end
22
+
23
+ string
24
+ end
25
+
26
+ def valid?(string)
27
+ string = string.number if string.is_a?(Twilio::Rails::PhoneNumber)
28
+ !!string&.match?(PHONE_NUMBER_REGEX)
29
+ end
30
+
31
+ def to_param(string)
32
+ string = coerce(string)
33
+ return "" unless string
34
+ matches = string.match(PHONE_NUMBER_SEGMENTS_REGEX)
35
+ raise Twilio::Rails::Error, "[to_param] Phone number marked as valid but could not capture. I made a bad regex: #{string}" unless matches
36
+ matches.captures.join("-")
37
+ end
38
+
39
+ def display(string)
40
+ coerced_phone_number = coerce(string)
41
+ if coerced_phone_number
42
+ matches = coerced_phone_number.match(PHONE_NUMBER_SEGMENTS_REGEX)
43
+ raise Twilio::Rails::Error, "[display] Phone number marked as valid but could not capture. I made a bad regex: #{string}" unless matches
44
+ "(#{matches.captures[0]}) #{matches.captures[1]} #{matches.captures[2]}"
45
+ else
46
+ string
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twilio
4
+ module Rails
5
+ # The interface for the phone number validator and formatter that is defined in the `Twilio::Rails.config.phone_number_formatter`
6
+ # configuration. This delegates the methods to that instance and is used both internally to the gem and by the gem consumer.
7
+ module PhoneNumberFormatter
8
+ extend self
9
+ extend Forwardable
10
+
11
+ def_delegators :formatter, :coerce, :valid?, :to_param, :display
12
+
13
+ private
14
+
15
+ def formatter
16
+ Twilio::Rails.config.phone_number_formatter
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Twilio
3
4
  module Rails
4
5
  class Railtie < ::Rails::Railtie
5
6
  config.before_initialize do
6
7
  ActiveSupport::Inflector.inflections(:en) do |inflect|
7
- inflect.acronym 'SMS'
8
+ inflect.acronym "SMS"
8
9
  end
9
10
  end
10
11
 
@@ -12,6 +13,10 @@ module Twilio
12
13
  # TODO: This should work but it does not. I think maybe it happens too late? The same line works if you add it directly to the `application.rb` of the app. It is needed for dev mode.
13
14
  # application.config.hosts << Twilio::Rails.config.host_domain
14
15
  end
16
+
17
+ initializer "twilio_rails.deprecator" do |app|
18
+ app.deprecators[:twilio_rails] = Twilio::Rails.deprecator
19
+ end
15
20
  end
16
21
  end
17
22
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Twilio
3
4
  module Rails
4
5
  module SMS
@@ -34,7 +35,7 @@ module Twilio
34
35
  #
35
36
  # @return [String] the name of the responder.
36
37
  def responder_name
37
- self.name.demodulize.underscore.gsub(/_responder\Z/, "")
38
+ name.demodulize.underscore.gsub(/_responder\Z/, "")
38
39
  end
39
40
  end
40
41
 
@@ -43,22 +44,22 @@ module Twilio
43
44
  @sms_conversation = message.sms_conversation
44
45
  end
45
46
 
46
- # Must be implemented by the subclass otherwise will raise a `NotImplementedError`. Returns true if this
47
+ # Must be implemented by the subclass otherwise will raise a `NoMethodError`. Returns true if this
47
48
  # responder should handle the given message. If true then the {#reply} method will be called to generate the
48
49
  # body of the response. It has access to the message and the conversation.
49
50
  #
50
51
  # @return [true, false] true if this responder should handle the given message.
51
52
  def handle?
52
- raise NotImplementedError
53
+ raise NoMethodError, "#{self.class}#handle? must be implemented."
53
54
  end
54
55
 
55
- # Must be implemented by the subclass otherwise will raise a `NotImplementedError`. Returns the body of the
56
+ # Must be implemented by the subclass otherwise will raise a `NoMethodError`. Returns the body of the
56
57
  # message to be sent in response. Will only be called if {#handle?} returns true. It has access to the message
57
58
  # and the conversation.
58
59
  #
59
60
  # @return [String, nil] the body of the response to be sent as SMS, or `nil` if no message should be sent.
60
61
  def reply
61
- raise NotImplementedError
62
+ raise NoMethodError, "#{self.class}#reply must be implemented."
62
63
  end
63
64
 
64
65
  protected
@@ -1,5 +1,5 @@
1
-
2
1
  # frozen_string_literal: true
2
+
3
3
  module Twilio
4
4
  module Rails
5
5
  module SMS
@@ -25,7 +25,8 @@ module Twilio
25
25
  return responder.reply if responder.handle?
26
26
  end
27
27
 
28
- raise Twilio::Rails::SMS::InvalidResponderError, "No responder found for message_id=#{ message.id } : #{ message.body }"
28
+ raise Twilio::Rails::SMS::InvalidResponderError, "No responder found for SMS. message_id=#{message.id} " \
29
+ "phone_caller_id=#{sms_conversation.phone_caller&.id} from_number=\"#{sms_conversation.from_number}\" body=\"#{message.body}\""
29
30
  end
30
31
  end
31
32
  end
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Twilio
3
4
  module Rails
4
5
  module SMS
5
6
  # Base error class for errors relating to Twilio phone interactions.
6
- class Error < ::Twilio::Rails::Error ; end
7
+ class Error < ::Twilio::Rails::Error; end
7
8
 
8
9
  # Error raised when a responder is unable to handle an SMS message.
9
- class InvalidResponderError < Error ; end
10
+ class InvalidResponderError < Error; end
10
11
  end
11
12
  end
12
13
  end
@@ -1,5 +1,5 @@
1
1
  module Twilio
2
2
  module Rails
3
- VERSION = "1.0.1"
3
+ VERSION = "1.1.0"
4
4
  end
5
5
  end