twilio-rails 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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