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.
- checksums.yaml +4 -4
- data/README.md +23 -17
- data/app/controllers/twilio/rails/phone_controller.rb +6 -2
- data/app/controllers/twilio/rails/sms_controller.rb +6 -5
- data/app/jobs/twilio/rails/phone/attach_recording_job.rb +1 -0
- data/app/jobs/twilio/rails/phone/finished_call_job.rb +1 -0
- data/app/jobs/twilio/rails/phone/unanswered_call_job.rb +1 -0
- data/app/operations/twilio/rails/application_operation.rb +2 -1
- data/app/operations/twilio/rails/find_or_create_phone_caller_operation.rb +2 -1
- data/app/operations/twilio/rails/phone/attach_recording_operation.rb +3 -2
- data/app/operations/twilio/rails/phone/base_operation.rb +1 -0
- data/app/operations/twilio/rails/phone/create_operation.rb +11 -8
- data/app/operations/twilio/rails/phone/find_operation.rb +1 -0
- data/app/operations/twilio/rails/phone/finished_call_operation.rb +2 -1
- data/app/operations/twilio/rails/phone/receive_recording_operation.rb +3 -2
- data/app/operations/twilio/rails/phone/start_call_operation.rb +15 -13
- data/app/operations/twilio/rails/phone/twiml/after_operation.rb +2 -1
- data/app/operations/twilio/rails/phone/twiml/base_operation.rb +11 -5
- data/app/operations/twilio/rails/phone/twiml/error_operation.rb +2 -1
- data/app/operations/twilio/rails/phone/twiml/greeting_operation.rb +3 -2
- data/app/operations/twilio/rails/phone/twiml/invalid_phone_number_operation.rb +25 -0
- data/app/operations/twilio/rails/phone/twiml/prompt_operation.rb +6 -6
- data/app/operations/twilio/rails/phone/twiml/prompt_response_operation.rb +2 -1
- data/app/operations/twilio/rails/phone/twiml/request_validation_failure_operation.rb +1 -0
- data/app/operations/twilio/rails/phone/twiml/timeout_operation.rb +5 -4
- data/app/operations/twilio/rails/phone/unanswered_call_operation.rb +2 -1
- data/app/operations/twilio/rails/phone/update_operation.rb +1 -0
- data/app/operations/twilio/rails/phone/update_response_operation.rb +1 -0
- data/app/operations/twilio/rails/sms/base_operation.rb +1 -0
- data/app/operations/twilio/rails/sms/create_operation.rb +2 -1
- data/app/operations/twilio/rails/sms/find_message_operation.rb +1 -0
- data/app/operations/twilio/rails/sms/find_operation.rb +1 -0
- data/app/operations/twilio/rails/sms/send_operation.rb +12 -12
- data/app/operations/twilio/rails/sms/twiml/base_operation.rb +1 -0
- data/app/operations/twilio/rails/sms/twiml/error_operation.rb +1 -0
- data/app/operations/twilio/rails/sms/twiml/message_operation.rb +4 -3
- data/app/operations/twilio/rails/sms/update_message_operation.rb +1 -0
- data/lib/generators/twilio/rails/install/install_generator.rb +1 -0
- data/lib/generators/twilio/rails/install/templates/initializer.rb +5 -8
- data/lib/generators/twilio/rails/install/templates/message.rb +1 -0
- data/lib/generators/twilio/rails/install/templates/phone_call.rb +1 -0
- data/lib/generators/twilio/rails/install/templates/phone_caller.rb +1 -0
- data/lib/generators/twilio/rails/install/templates/recording.rb +1 -0
- data/lib/generators/twilio/rails/install/templates/response.rb +1 -0
- data/lib/generators/twilio/rails/install/templates/sms_conversation.rb +1 -0
- data/lib/generators/twilio/rails/phone_tree/phone_tree_generator.rb +1 -0
- data/lib/generators/twilio/rails/sms_responder/sms_responder_generator.rb +1 -0
- data/lib/generators/twilio/rails/sms_responder/templates/responder.rb.erb +2 -2
- data/lib/tasks/rails_tasks.rake +6 -5
- data/lib/twilio/rails/client.rb +9 -8
- data/lib/twilio/rails/concerns/has_direction.rb +2 -1
- data/lib/twilio/rails/concerns/has_phone_number.rb +13 -4
- data/lib/twilio/rails/concerns/has_time_scopes.rb +1 -0
- data/lib/twilio/rails/configuration.rb +46 -42
- data/lib/twilio/rails/formatter.rb +35 -29
- data/lib/twilio/rails/models/message.rb +1 -0
- data/lib/twilio/rails/models/phone_call.rb +1 -0
- data/lib/twilio/rails/models/phone_caller.rb +4 -3
- data/lib/twilio/rails/models/recording.rb +1 -0
- data/lib/twilio/rails/models/response.rb +7 -6
- data/lib/twilio/rails/models/sms_conversation.rb +1 -0
- data/lib/twilio/rails/phone/base_tree.rb +8 -8
- data/lib/twilio/rails/phone/tree.rb +8 -8
- data/lib/twilio/rails/phone/tree_macros.rb +27 -8
- data/lib/twilio/rails/phone.rb +3 -2
- data/lib/twilio/rails/phone_number.rb +6 -5
- data/lib/twilio/rails/phone_number_formatter/north_america.rb +52 -0
- data/lib/twilio/rails/phone_number_formatter.rb +20 -0
- data/lib/twilio/rails/railtie.rb +6 -1
- data/lib/twilio/rails/sms/delegated_responder.rb +6 -5
- data/lib/twilio/rails/sms/responder.rb +3 -2
- data/lib/twilio/rails/sms.rb +3 -2
- data/lib/twilio/rails/version.rb +1 -1
- data/lib/twilio/rails.rb +12 -26
- 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
|
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 = [
|
20
|
-
@no_responses = [
|
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
|
-
"#{
|
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
|
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, "`
|
219
|
-
raise Error,
|
220
|
-
raise Error,
|
221
|
-
raise Error, "`
|
222
|
-
raise Error, "`
|
223
|
-
raise Error, "`
|
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 =
|
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("@#{
|
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, "
|
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
|
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
|
337
|
-
raise(error_class, "Responder class '#{
|
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 #{
|
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 '#{
|
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
|
362
|
-
raise(error_class, "Tree class '#{
|
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 #{
|
365
|
-
raise(error_class, "Tree is not a Twilio::Rails::Phone::BaseTree class #{
|
366
|
-
raise(error_class, "Tree is not a 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 '#{
|
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
|
-
|
18
|
-
|
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
|
-
|
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
|
-
|
37
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
@@ -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::
|
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(
|
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
|
-
|
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
|
@@ -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: {
|
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
|
-
|
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?(
|
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?(
|
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
|
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 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
|
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
|
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.
|
189
|
-
# important case here is a number from outside of North America. This
|
190
|
-
#
|
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
|
-
|
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
|
-
"#{
|
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
|
-
"#{
|
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 :
|
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 #{
|
154
|
-
raise Twilio::Rails::Phone::InvalidTreeError, "pause: must be over zero but is #{
|
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 #{
|
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 #{
|
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.
|
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:
|
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| "#{
|
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
|
-
"#{
|
131
|
+
"#{::Twilio::Rails.config.host}/#{filename}"
|
113
132
|
else
|
114
|
-
raise Twilio::Rails::Phone::Error, "Cannot find public file '#{
|
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
|
-
{
|
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
|