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.
- checksums.yaml +4 -4
- data/README.md +20 -14
- 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 -5
- 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 +42 -40
- 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 -7
- data/lib/twilio/rails/phone/tree.rb +8 -7
- data/lib/twilio/rails/phone/tree_macros.rb +9 -9
- 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 +20 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e5bf8143247d793c1fde606c166165114365e54f1e5f4f33aa3642ca58563e97
|
4
|
+
data.tar.gz: f6b35b2bce17b4240ea2f63705e095dedea6aacf9d321321719462126419d6c1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1cafe99962f6610985140f2f8a553bf7521c67cb449be1ead374245f94d66b6bfca04e7200d7c9421f8d0c522eee1d543ffe41983ba80df3e5a41141021df75b
|
7
|
+
data.tar.gz: 345023826a58a7bd9718a5ef1ed5ef06114027f01082a3c5a56b9c50824c4ad028b69f8293794e0d428a1e70c1d062b01367ef48b29aac45f74e8b0135ce35a0
|
data/README.md
CHANGED
@@ -8,8 +8,18 @@ The `twilio-rails` gem is an opinionated Rails engine and a framework for buildi
|
|
8
8
|
The most powerful ability of this engine is to build phone trees (think of calling customer service and pressing 2 for account information or whatever) using a simple Ruby DSL.
|
9
9
|
|
10
10
|
What does this mean in practice? **Call and find out!**
|
11
|
-
|
12
|
-
|
11
|
+
|
12
|
+
The [`twilio-rails-example`](https://github.com/kmcphillips/twilio-rails-example) app is running here:
|
13
|
+
|
14
|
+
* **🇨🇦 Calling from Canada:** 📞 (204) 800-7772
|
15
|
+
* **🇺🇸 Calling from the US:** 📞 (631) 800-7772
|
16
|
+
* **Internationally:** [Sorry, not yet supported](https://github.com/kmcphillips/twilio-rails#limitations-and-known-issues)
|
17
|
+
|
18
|
+
There is also a more fun but less cleanly organized [`dial-a-haiku`](https://github.com/kmcphillips/dial-a-haiku) running here:
|
19
|
+
* **🇨🇦 Calling from Canada:** 📞 (249) 444-2458 / (249)44-HAIKU
|
20
|
+
* **🇺🇸 Calling from the US:** 📞 (341) 444-2458 / (341)44-HAIKU
|
21
|
+
* **Internationally:** [Sorry, not yet supported](https://github.com/kmcphillips/twilio-rails#limitations-and-known-issues)
|
22
|
+
|
13
23
|
|
14
24
|
## Documentation
|
15
25
|
|
@@ -103,10 +113,11 @@ Both are explained in detail below.
|
|
103
113
|
|
104
114
|
### Example app
|
105
115
|
|
106
|
-
An example Rails app demonstrating the framework is available at [`twilio-rails-example`](https://github.com/kmcphillips/twilio-rails-example). It can be run locally with some minimal configuration, or can be reached as a working Twilio app by calling:
|
116
|
+
An example Rails app demonstrating the framework is available at [`twilio-rails-example`](https://github.com/kmcphillips/twilio-rails-example). The seteps to go from new Rails app to running Twilio application are broken down into well documented commits. It's a great place to learn and experiment. It can be run locally with some minimal configuration, or can be reached as a working Twilio app by calling:
|
107
117
|
|
108
|
-
*
|
109
|
-
*
|
118
|
+
* **🇨🇦 Calling from Canada:** 📞 (204) 800-7772
|
119
|
+
* **🇺🇸 Calling from the US:** 📞 (631) 800-7772
|
120
|
+
* **Internationally:** [Sorry, not yet supported](https://github.com/kmcphillips/twilio-rails#limitations-and-known-issues)
|
110
121
|
|
111
122
|
|
112
123
|
## How it works
|
@@ -274,7 +285,7 @@ prompt :record_your_feedback,
|
|
274
285
|
|
275
286
|
The above `gather:` with `type: :voice` example will finish reading the message and then record the phone caller's speech for 30 seconds or until they press the `#` pound key. The phone tree will then immediately execute the `after:`, while the framework continues to handle the audio recording asynchronously. When Twilio makes it available, the audio file of the recording will be downloaded and stored as an ActiveStorage attachment in a `Recording` model as `response.recording`. If the `transcribe:` option is set to `true`, the voice in the recording will also attempt to be transcribed as text and stored as `response.transcription`. Importantly though, **neither are guaranteed to arrive or will arrive immediately**. In practice they both usually arrive within a few seconds, but can sometimes be blank or missing if the caller is silent or garbled. There is a cost to transcription so it can be disabled, and the `profanity_filter:` defaults to false and will just *** out any profanity in the transcription.
|
276
287
|
|
277
|
-
Finally, the `gather:` can also accept `type: :speech` which is a specialzed model designed to identify voice in realtime. It will provide the `response.transcription` field immediately, making it available in the `after:` proc or in the next prompt. But the tradeoffs are that it does not provide a recording, there is a time gap of a few seconds between prompts, and it is more expensive. See the [Twilio documentation for specifics](https://www.twilio.com/docs/voice/twiml/gather#speechmodel). The keys it expects match the documentation, `speech_model:`, `speech_timeout:`, `language:` (defaults to "en-US"), and `
|
288
|
+
Finally, the `gather:` can also accept `type: :speech` which is a specialzed model designed to identify voice in realtime. It will provide the `response.transcription` field immediately, making it available in the `after:` proc or in the next prompt. But the tradeoffs are that it does not provide a recording, there is a time gap of a few seconds between prompts, and it is more expensive. See the [Twilio documentation for specifics](https://www.twilio.com/docs/voice/twiml/gather#speechmodel). The keys it expects match the documentation, `speech_model:`, `speech_timeout:`, `language:` (defaults to "en-US"), and `enhanced:` (defaults to false).
|
278
289
|
|
279
290
|
```ruby
|
280
291
|
prompt :what_direction_should_we_go,
|
@@ -308,7 +319,7 @@ prompt :what_direction_should_we_go,
|
|
308
319
|
}
|
309
320
|
```
|
310
321
|
|
311
|
-
To inspect the implementation and get further detail, most of the magic happens in [`Twilio::Rails::Phone::Tree`](lib/twilio/rails/phone/tree.rb) and the operations under [`Twilio::Rails::Phone::Twiml`](app/operations/twilio/rails/phone/twiml/) where the DSL is defined and then converted
|
322
|
+
To inspect the implementation and get further detail, most of the magic happens in [`Twilio::Rails::Phone::Tree`](lib/twilio/rails/phone/tree.rb) and the operations under [`Twilio::Rails::Phone::Twiml`](app/operations/twilio/rails/phone/twiml/) where the DSL is defined and then converted into [TwiML](https://www.twilio.com/docs/voice/twiml).
|
312
323
|
|
313
324
|
|
314
325
|
### Make an outgoing phone call
|
@@ -326,7 +337,7 @@ Twilio::Phone::StartCallOperation.call(
|
|
326
337
|
|
327
338
|
### SMS responders
|
328
339
|
|
329
|
-
>
|
340
|
+
> [!IMPORTANT]
|
330
341
|
> Due to how Twilio makes API calls into the application for SMS messages, SMS responders require Rails sessions to be enabled and setup in order to handle SMS messages.
|
331
342
|
|
332
343
|
Twilio provides a hook for incoming SMS messages and can send SMS messages to any phone number. This gem provides a simple method for handling SMS conversations, though it does not provide a full stateful tree structure.
|
@@ -365,13 +376,8 @@ phone_caller = Twilio::Rails::FindOrCreatePhoneCallerOperation.call(phone_number
|
|
365
376
|
|
366
377
|
All errors are subclasses of [`Twilio::Rails::Error`](lib/twilio/rails.rb). They are grouped under [`Twilio::Rails::Phone::Error`](lib/twilio/rails/phone.rb) and [`Twilio::Rails::SMS::Error`](lib/twilio/rails/sms.rb), and then further specialized from there.
|
367
378
|
|
368
|
-
There
|
379
|
+
There are a few places where exceptions are notified from inside the framework using `::Rails.error.report`. They are never rescued or handled. See the [Rails documentation](https://api.rubyonrails.org/classes/ActiveSupport/ErrorReporter.html) for how to use the error reporter.
|
369
380
|
|
370
|
-
```ruby
|
371
|
-
config.exception_notifier = ->(exception, message, context, exception_binding) {
|
372
|
-
# Send an email or use some kind of service etc.
|
373
|
-
}
|
374
|
-
```
|
375
381
|
|
376
382
|
### The rest of the documentation
|
377
383
|
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Twilio
|
3
4
|
module Rails
|
4
5
|
class PhoneController < ApplicationController
|
@@ -10,6 +11,9 @@ module Twilio
|
|
10
11
|
respond_to do |format|
|
11
12
|
format.xml do
|
12
13
|
phone_call = Twilio::Rails::Phone::CreateOperation.call(params: params_hash, tree: tree)
|
14
|
+
rescue
|
15
|
+
render xml: Twilio::Rails::Phone::Twiml::InvalidPhoneNumberOperation.call(tree: tree)
|
16
|
+
else
|
13
17
|
render xml: Twilio::Rails::Phone::Twiml::GreetingOperation.call(phone_call_id: phone_call.id, tree: tree)
|
14
18
|
end
|
15
19
|
end
|
@@ -39,7 +43,7 @@ module Twilio
|
|
39
43
|
format.xml do
|
40
44
|
phone_call = Twilio::Rails::Phone::FindOperation.call(params: params_hash)
|
41
45
|
phone_call = Twilio::Rails::Phone::UpdateOperation.call(phone_call_id: phone_call.id, params: params_hash)
|
42
|
-
|
46
|
+
Twilio::Rails::Phone::UpdateResponseOperation.call(phone_call_id: phone_call.id, response_id: params[:response_id].to_i, params: params_hash)
|
43
47
|
render xml: Twilio::Rails::Phone::Twiml::PromptResponseOperation.call(phone_call_id: phone_call.id, tree: tree, response_id: params[:response_id].to_i, params: params_hash)
|
44
48
|
end
|
45
49
|
end
|
@@ -70,7 +74,7 @@ module Twilio
|
|
70
74
|
respond_to do |format|
|
71
75
|
format.xml do
|
72
76
|
phone_call = Twilio::Rails::Phone::FindOperation.call(params: params_hash)
|
73
|
-
|
77
|
+
Twilio::Rails::Phone::UpdateOperation.call(phone_call_id: phone_call.id, params: params_hash)
|
74
78
|
|
75
79
|
head :ok
|
76
80
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Twilio
|
3
4
|
module Rails
|
4
5
|
class SMSController < ::Twilio::Rails::ApplicationController
|
@@ -10,7 +11,7 @@ module Twilio
|
|
10
11
|
respond_to do |format|
|
11
12
|
format.xml do
|
12
13
|
if spam?
|
13
|
-
render xml: Twilio::Rails::SMS::Twiml::ErrorOperation.call
|
14
|
+
render xml: Twilio::Rails::SMS::Twiml::ErrorOperation.call
|
14
15
|
else
|
15
16
|
if session[:sms_conversation_id].present?
|
16
17
|
conversation = Twilio::Rails::SMS::FindOperation.call(sms_conversation_id: session[:sms_conversation_id])
|
@@ -29,10 +30,10 @@ module Twilio
|
|
29
30
|
respond_to do |format|
|
30
31
|
format.xml do
|
31
32
|
if params[:message_id].present?
|
32
|
-
|
33
|
+
Twilio::Rails::SMS::UpdateMessageOperation.call(message_id: params[:message_id].to_i, params: params_hash)
|
33
34
|
else
|
34
35
|
message = Twilio::Rails::SMS::FindMessageOperation.call(params: params_hash)
|
35
|
-
|
36
|
+
Twilio::Rails::SMS::UpdateMessageOperation.call(message_id: message.id, params: params_hash)
|
36
37
|
end
|
37
38
|
|
38
39
|
head :ok
|
@@ -46,14 +47,14 @@ module Twilio
|
|
46
47
|
if params["AccountSid"] != Twilio::Rails.config.account_sid
|
47
48
|
respond_to do |format|
|
48
49
|
format.xml do
|
49
|
-
render xml: Twilio::Rails::SMS::Twiml::ErrorOperation.call
|
50
|
+
render xml: Twilio::Rails::SMS::Twiml::ErrorOperation.call
|
50
51
|
end
|
51
52
|
end
|
52
53
|
end
|
53
54
|
end
|
54
55
|
|
55
56
|
def spam?
|
56
|
-
Twilio::Rails.config.spam_filter
|
57
|
+
Twilio::Rails.config.spam_filter&.call(params)
|
57
58
|
end
|
58
59
|
|
59
60
|
def params_hash
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Twilio
|
3
4
|
module Rails
|
4
5
|
class ApplicationOperation < ActiveOperation::Base
|
@@ -8,7 +9,7 @@ module Twilio
|
|
8
9
|
executable.call
|
9
10
|
@operation_runtime_stop = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
10
11
|
|
11
|
-
::Twilio::Rails.config.logger.tagged(self.class) { |l| l.info("execution time #{
|
12
|
+
::Twilio::Rails.config.logger.tagged(self.class) { |l| l.info("execution time #{instance.operation_runtime_seconds} seconds") }
|
12
13
|
end
|
13
14
|
|
14
15
|
protected
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Twilio
|
3
4
|
module Rails
|
4
5
|
# Finds and returns a {Twilio::Rails::Models::PhoneCaller} by phone number, or creates a new one if it does not
|
@@ -22,7 +23,7 @@ module Twilio
|
|
22
23
|
private
|
23
24
|
|
24
25
|
def valid_phone_number
|
25
|
-
Twilio::Rails::
|
26
|
+
Twilio::Rails::PhoneNumberFormatter.coerce(phone_number)
|
26
27
|
end
|
27
28
|
end
|
28
29
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Twilio
|
3
4
|
module Rails
|
4
5
|
module Phone
|
@@ -12,7 +13,7 @@ module Twilio
|
|
12
13
|
|
13
14
|
if !recording.audio.attached?
|
14
15
|
if recording.url.blank?
|
15
|
-
raise Twilio::Rails::Phone::Error, "[AttachRecordingOperation] Has a blank URL and cannot be fetched recording_id=#{
|
16
|
+
raise Twilio::Rails::Phone::Error, "[AttachRecordingOperation] Has a blank URL and cannot be fetched recording_id=#{recording.id}"
|
16
17
|
end
|
17
18
|
|
18
19
|
response = Faraday.get(recording.url)
|
@@ -21,7 +22,7 @@ module Twilio
|
|
21
22
|
recording.audio.attach(io: StringIO.new(response.body), filename: "recording.wav", content_type: "audio/wav")
|
22
23
|
recording.save!
|
23
24
|
else
|
24
|
-
raise Twilio::Rails::Phone::Error, "[AttachRecordingOperation] Failed to fetch recording recording_id=#{
|
25
|
+
raise Twilio::Rails::Phone::Error, "[AttachRecordingOperation] Failed to fetch recording recording_id=#{recording.id} HTTP#{response.status} from #{recording.url}"
|
25
26
|
end
|
26
27
|
end
|
27
28
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Twilio
|
3
4
|
module Rails
|
4
5
|
module Phone
|
@@ -16,19 +17,19 @@ module Twilio
|
|
16
17
|
from_number: params["Caller"].presence || params["From"].presence,
|
17
18
|
from_city: params["CallerCity"].presence || params["FromCity"].presence,
|
18
19
|
from_province: params["CallerState"].presence || params["FromState"].presence,
|
19
|
-
from_country: params["CallerCountry"].presence || params["FromCountry"].presence
|
20
|
+
from_country: params["CallerCountry"].presence || params["FromCountry"].presence
|
20
21
|
)
|
21
22
|
|
22
23
|
phone_caller = Twilio::Rails::FindOrCreatePhoneCallerOperation.call(phone_number: phone_call.from_number)
|
23
24
|
|
24
25
|
if !phone_caller
|
25
|
-
error_message = if !Twilio::Rails::
|
26
|
+
error_message = if !Twilio::Rails::PhoneNumberFormatter.valid?(phone_call.from_number)
|
26
27
|
"The phone number is invalid."
|
27
28
|
else
|
28
29
|
"The phone caller could not be persisted or retrieved."
|
29
30
|
end
|
30
31
|
|
31
|
-
raise Twilio::Rails::Phone::Error, "Failed to handle incoming Twilio phone call. #{
|
32
|
+
raise Twilio::Rails::Phone::Error, "Failed to handle incoming Twilio phone call. #{error_message} phone_number=#{phone_call.from_number} call_sid=#{params["CallSid"]}"
|
32
33
|
end
|
33
34
|
|
34
35
|
phone_call.phone_caller = phone_caller
|
@@ -36,11 +37,13 @@ module Twilio
|
|
36
37
|
|
37
38
|
phone_call
|
38
39
|
rescue => e
|
39
|
-
|
40
|
-
|
41
|
-
context: {
|
42
|
-
|
43
|
-
|
40
|
+
::Rails.error.report(e,
|
41
|
+
handled: false,
|
42
|
+
context: {
|
43
|
+
message: "Failed to handle incoming Twilio phone call.",
|
44
|
+
params: params,
|
45
|
+
tree: tree
|
46
|
+
})
|
44
47
|
raise
|
45
48
|
end
|
46
49
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Twilio
|
3
4
|
module Rails
|
4
5
|
module Phone
|
@@ -8,7 +9,7 @@ module Twilio
|
|
8
9
|
Twilio::Rails.config.logger.tagged(self.class) { |l| l.warn("Skipping duplicate finished call job") }
|
9
10
|
else
|
10
11
|
phone_call.update!(finished: true)
|
11
|
-
phone_call.tree.finished_call
|
12
|
+
phone_call.tree.finished_call&.call(phone_call)
|
12
13
|
end
|
13
14
|
end
|
14
15
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Twilio
|
3
4
|
module Rails
|
4
5
|
module Phone
|
@@ -10,12 +11,12 @@ module Twilio
|
|
10
11
|
response = phone_call.responses.find(response_id)
|
11
12
|
|
12
13
|
if phone_call.recordings.sid(params["RecordingSid"]).any?
|
13
|
-
Twilio::Rails.config.logger.tagged(self.class) { |l| l.warn("duplicate recording for response_id=#{
|
14
|
+
Twilio::Rails.config.logger.tagged(self.class) { |l| l.warn("duplicate recording for response_id=#{response.id} recording_sid=#{params["RecordingSid"]}") }
|
14
15
|
else
|
15
16
|
recording = phone_call.recordings.build(
|
16
17
|
recording_sid: params["RecordingSid"],
|
17
18
|
url: params["RecordingUrl"],
|
18
|
-
duration: params["RecordingDuration"].presence
|
19
|
+
duration: params["RecordingDuration"].presence
|
19
20
|
)
|
20
21
|
recording.save!
|
21
22
|
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Twilio
|
3
4
|
module Rails
|
4
5
|
module Phone
|
@@ -21,31 +22,32 @@ module Twilio
|
|
21
22
|
"CallSid" => nil,
|
22
23
|
"direction" => "outbound",
|
23
24
|
"To" => from,
|
24
|
-
"From" => to
|
25
|
+
"From" => to
|
25
26
|
}
|
26
27
|
|
27
28
|
begin
|
28
29
|
sid = Twilio::Rails::Client.start_call(url: tree.outbound_url, to: to, from: from, answering_machine_detection: answering_machine_detection)
|
29
30
|
params["CallSid"] = sid
|
30
31
|
rescue Twilio::REST::TwilioError => e
|
31
|
-
|
32
|
-
|
33
|
-
context: {
|
34
|
-
|
35
|
-
|
32
|
+
::Rails.error.report(e,
|
33
|
+
handled: false,
|
34
|
+
context: {
|
35
|
+
message: "Failed to start Twilio phone call. Got REST error response.",
|
36
|
+
params: params
|
37
|
+
})
|
36
38
|
raise
|
37
39
|
rescue => e
|
38
|
-
|
39
|
-
|
40
|
-
context: {
|
41
|
-
|
42
|
-
|
40
|
+
::Rails.error.report(e,
|
41
|
+
handled: false,
|
42
|
+
context: {
|
43
|
+
message: "Failed to start Twilio phone call. Got unknown error.",
|
44
|
+
params: params
|
45
|
+
})
|
43
46
|
raise
|
44
47
|
end
|
45
48
|
|
46
49
|
# TODO: I think this may be a race condition
|
47
|
-
|
48
|
-
phone_call
|
50
|
+
Twilio::Rails::Phone::CreateOperation.call(params: params, tree: tree)
|
49
51
|
end
|
50
52
|
end
|
51
53
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Twilio
|
3
4
|
module Rails
|
4
5
|
module Phone
|
@@ -27,7 +28,7 @@ module Twilio
|
|
27
28
|
end
|
28
29
|
end
|
29
30
|
|
30
|
-
Twilio::Rails.config.logger.info("after_twiml: #{twiml_response
|
31
|
+
Twilio::Rails.config.logger.info("after_twiml: #{twiml_response}")
|
31
32
|
twiml_response.to_s
|
32
33
|
end
|
33
34
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Twilio
|
3
4
|
module Rails
|
4
5
|
module Phone
|
@@ -20,17 +21,17 @@ module Twilio
|
|
20
21
|
message = message.call(response) if message.is_a?(Proc)
|
21
22
|
message = Twilio::Rails::Phone::Tree::Message.new(**message) if message.is_a?(Hash)
|
22
23
|
next if message.blank?
|
23
|
-
message = Twilio::Rails::Phone::Tree::Message.new(say: message, voice:
|
24
|
+
message = Twilio::Rails::Phone::Tree::Message.new(say: message, voice: voice) if message.is_a?(String)
|
24
25
|
|
25
|
-
raise Twilio::Rails::Phone::InvalidTreeError "unknown message #{
|
26
|
+
raise Twilio::Rails::Phone::InvalidTreeError "unknown message #{message} is a #{message.class}" unless message.is_a?(Twilio::Rails::Phone::Tree::Message)
|
26
27
|
|
27
28
|
# TODO: if we want to make a transcript of sent messages this is where we would record outgoing messages
|
28
29
|
|
29
30
|
if message.say?
|
30
31
|
value = message.value
|
31
32
|
value = message.value.call(response) if message.value.is_a?(Proc)
|
32
|
-
twiml.say(voice: message.voice ||
|
33
|
-
message.block
|
33
|
+
twiml.say(voice: message.voice || voice, message: value) do |say|
|
34
|
+
message.block&.call(say)
|
34
35
|
end
|
35
36
|
elsif message.play?
|
36
37
|
value = message.value
|
@@ -39,10 +40,15 @@ module Twilio
|
|
39
40
|
elsif message.pause?
|
40
41
|
twiml.pause(length: message.value)
|
41
42
|
else
|
42
|
-
raise Twilio::Rails::Phone::InvalidTreeError, "unknown message #{
|
43
|
+
raise Twilio::Rails::Phone::InvalidTreeError, "unknown message #{message}"
|
43
44
|
end
|
44
45
|
end
|
45
46
|
end
|
47
|
+
|
48
|
+
# @return [String] the voice to use for the TwiML response if configured on the tree.
|
49
|
+
def voice
|
50
|
+
tree.config[:voice] if tree
|
51
|
+
end
|
46
52
|
end
|
47
53
|
end
|
48
54
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Twilio
|
3
4
|
module Rails
|
4
5
|
module Phone
|
@@ -12,7 +13,7 @@ module Twilio
|
|
12
13
|
add_messages(twiml, message_set: messages, response: phone_call.responses.build) if messages
|
13
14
|
twiml.hangup
|
14
15
|
|
15
|
-
Twilio::Rails.config.logger.info("error_twiml: #{twiml
|
16
|
+
Twilio::Rails.config.logger.info("error_twiml: #{twiml}")
|
16
17
|
twiml.to_s
|
17
18
|
end
|
18
19
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Twilio
|
3
4
|
module Rails
|
4
5
|
module Phone
|
@@ -7,8 +8,8 @@ module Twilio
|
|
7
8
|
input :tree, accepts: Twilio::Rails::Phone::Tree, type: :keyword, required: true
|
8
9
|
|
9
10
|
def execute
|
10
|
-
if !phone_caller.
|
11
|
-
Twilio::Rails::Phone::Twiml::
|
11
|
+
if !phone_caller.valid_phone_number? && tree.config[:invalid_phone_number]
|
12
|
+
Twilio::Rails::Phone::Twiml::InvalidPhoneNumberOperation.call(phone_call_id: phone_call.id, tree: tree)
|
12
13
|
else
|
13
14
|
after = tree.greeting
|
14
15
|
after = Twilio::Rails::Phone::Tree::After.new(after.proc.call(phone_call.responses.build)) if after.proc
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Twilio
|
4
|
+
module Rails
|
5
|
+
module Phone
|
6
|
+
module Twiml
|
7
|
+
class InvalidPhoneNumberOperation < Twilio::Rails::Phone::Twiml::BaseOperation
|
8
|
+
input :tree, accepts: Twilio::Rails::Phone::Tree, type: :keyword, required: true
|
9
|
+
input :phone_call_id, accepts: Integer, type: :keyword, required: false, default: nil
|
10
|
+
|
11
|
+
def execute
|
12
|
+
twiml = Twilio::TwiML::VoiceResponse.new
|
13
|
+
messages = tree.config[:invalid_phone_number] || []
|
14
|
+
response = phone_call.responses.build if phone_call_id
|
15
|
+
add_messages(twiml, message_set: messages, response: response)
|
16
|
+
twiml.hangup
|
17
|
+
|
18
|
+
Twilio::Rails.config.logger.info("error_twiml: #{twiml}")
|
19
|
+
twiml.to_s
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Twilio
|
3
4
|
module Rails
|
4
5
|
module Phone
|
@@ -12,7 +13,7 @@ module Twilio
|
|
12
13
|
|
13
14
|
response = phone_call.responses.find(response_id)
|
14
15
|
prompt = tree.prompts[response.prompt_handle]
|
15
|
-
raise Twilio::Rails::Phone::InvalidTreeError, "cannot find #{
|
16
|
+
raise Twilio::Rails::Phone::InvalidTreeError, "cannot find #{response.prompt_handle} in #{tree.name}" unless prompt
|
16
17
|
|
17
18
|
twiml_response = Twilio::TwiML::VoiceResponse.new do |twiml|
|
18
19
|
unless prompt.gather&.interrupt?
|
@@ -30,7 +31,7 @@ module Twilio
|
|
30
31
|
input: "dtmf",
|
31
32
|
num_digits: prompt.gather.args[:number],
|
32
33
|
timeout: prompt.gather.args[:timeout],
|
33
|
-
action_on_empty_result: false
|
34
|
+
action_on_empty_result: false
|
34
35
|
}
|
35
36
|
|
36
37
|
args[:finish_on_key] = prompt.gather.args[:finish_on_key] if prompt.gather.args[:finish_on_key]
|
@@ -57,7 +58,7 @@ module Twilio
|
|
57
58
|
),
|
58
59
|
recording_status_callback: ::Twilio::Rails::Engine.routes.url_helpers.phone_receive_recording_path(
|
59
60
|
response_id: response.id
|
60
|
-
)
|
61
|
+
)
|
61
62
|
}
|
62
63
|
|
63
64
|
if prompt.gather.args[:transcribe]
|
@@ -79,7 +80,7 @@ module Twilio
|
|
79
80
|
timeout: prompt.gather.args[:timeout],
|
80
81
|
action_on_empty_result: true,
|
81
82
|
language: prompt.gather.args[:language].presence || "en-US",
|
82
|
-
enhanced: !!prompt.gather.args[:enhanced]
|
83
|
+
enhanced: !!prompt.gather.args[:enhanced]
|
83
84
|
}
|
84
85
|
|
85
86
|
args[:speech_timeout] = prompt.gather.args[:speech_timeout] if prompt.gather.args[:speech_timeout]
|
@@ -98,7 +99,7 @@ module Twilio
|
|
98
99
|
end
|
99
100
|
end
|
100
101
|
|
101
|
-
Twilio::Rails.config.logger.info("prompt_twiml: #{twiml_response
|
102
|
+
Twilio::Rails.config.logger.info("prompt_twiml: #{twiml_response}")
|
102
103
|
twiml_response.to_s
|
103
104
|
end
|
104
105
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Twilio
|
3
4
|
module Rails
|
4
5
|
module Phone
|
@@ -15,7 +16,7 @@ module Twilio
|
|
15
16
|
response = Twilio::Rails::Phone::UpdateResponseOperation.call(params: params, response_id: response.id, phone_call_id: phone_call.id)
|
16
17
|
|
17
18
|
prompt = tree.prompts[response.prompt_handle]
|
18
|
-
raise Twilio::Rails::Phone::InvalidTreeError, "cannot find #{
|
19
|
+
raise Twilio::Rails::Phone::InvalidTreeError, "cannot find #{response.prompt_handle} in #{tree.name}" unless prompt
|
19
20
|
|
20
21
|
after = prompt.after
|
21
22
|
after = Twilio::Rails::Phone::Tree::After.new(after.proc.call(response)) if after.proc
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Twilio
|
3
4
|
module Rails
|
4
5
|
module Phone
|
@@ -20,12 +21,12 @@ module Twilio
|
|
20
21
|
twiml.hangup
|
21
22
|
end
|
22
23
|
|
23
|
-
Twilio::Rails.config.logger.info("final timeout on phone_call##{
|
24
|
-
Twilio::Rails.config.logger.info("timeout_twiml: #{twiml_response
|
24
|
+
Twilio::Rails.config.logger.info("final timeout on phone_call##{phone_call.id}")
|
25
|
+
Twilio::Rails.config.logger.info("timeout_twiml: #{twiml_response}")
|
25
26
|
twiml_response.to_s
|
26
27
|
else
|
27
28
|
prompt = tree.prompts[response.prompt_handle]
|
28
|
-
raise Twilio::Rails::Phone::InvalidTreeError, "cannot find #{
|
29
|
+
raise Twilio::Rails::Phone::InvalidTreeError, "cannot find #{response.prompt_handle} in #{tree.name}" unless prompt
|
29
30
|
|
30
31
|
after = prompt.after
|
31
32
|
after = Twilio::Rails::Phone::Tree::After.new(after.proc.call(response)) if after.proc
|
@@ -36,7 +37,7 @@ module Twilio
|
|
36
37
|
|
37
38
|
private
|
38
39
|
|
39
|
-
def final_timeout?(last_response, count:
|
40
|
+
def final_timeout?(last_response, count:)
|
40
41
|
responses = phone_call.responses.final_timeout_check(count: count, prompt_handle: last_response.prompt_handle)
|
41
42
|
|
42
43
|
responses.count == count && responses.all? { |r| r.timeout? }
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Twilio
|
3
4
|
module Rails
|
4
5
|
module Phone
|
@@ -13,7 +14,7 @@ module Twilio
|
|
13
14
|
Twilio::Rails.config.logger.tagged(self.class) { |l| l.warn("Skipping duplicate unanswered call job") }
|
14
15
|
else
|
15
16
|
phone_call.update!(unanswered: true)
|
16
|
-
phone_call.tree.unanswered_call
|
17
|
+
phone_call.tree.unanswered_call&.call(phone_call)
|
17
18
|
end
|
18
19
|
end
|
19
20
|
end
|