twilio-rails 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +0 -0
- data/LICENSE +21 -0
- data/README.md +413 -0
- data/Rakefile +8 -0
- data/app/assets/config/twilio_rails_manifest.js +1 -0
- data/app/assets/stylesheets/twilio/rails/application.css +15 -0
- data/app/controllers/twilio/rails/application_controller.rb +6 -0
- data/app/controllers/twilio/rails/phone_controller.rb +112 -0
- data/app/controllers/twilio/rails/sms_controller.rb +64 -0
- data/app/helpers/twilio/rails/application_helper.rb +6 -0
- data/app/jobs/twilio/rails/application_job.rb +6 -0
- data/app/jobs/twilio/rails/phone/attach_recording_job.rb +15 -0
- data/app/jobs/twilio/rails/phone/finished_call_job.rb +15 -0
- data/app/jobs/twilio/rails/phone/unanswered_call_job.rb +15 -0
- data/app/mailers/twilio/rails/application_mailer.rb +8 -0
- data/app/models/twilio/rails/application_record.rb +7 -0
- data/app/operations/twilio/rails/application_operation.rb +21 -0
- data/app/operations/twilio/rails/find_or_create_phone_caller_operation.rb +29 -0
- data/app/operations/twilio/rails/phone/attach_recording_operation.rb +31 -0
- data/app/operations/twilio/rails/phone/base_operation.rb +21 -0
- data/app/operations/twilio/rails/phone/create_operation.rb +49 -0
- data/app/operations/twilio/rails/phone/find_operation.rb +14 -0
- data/app/operations/twilio/rails/phone/finished_call_operation.rb +17 -0
- data/app/operations/twilio/rails/phone/receive_recording_operation.rb +35 -0
- data/app/operations/twilio/rails/phone/start_call_operation.rb +53 -0
- data/app/operations/twilio/rails/phone/twiml/after_operation.rb +37 -0
- data/app/operations/twilio/rails/phone/twiml/base_operation.rb +50 -0
- data/app/operations/twilio/rails/phone/twiml/error_operation.rb +22 -0
- data/app/operations/twilio/rails/phone/twiml/greeting_operation.rb +22 -0
- data/app/operations/twilio/rails/phone/twiml/prompt_operation.rb +109 -0
- data/app/operations/twilio/rails/phone/twiml/prompt_response_operation.rb +29 -0
- data/app/operations/twilio/rails/phone/twiml/request_validation_failure_operation.rb +16 -0
- data/app/operations/twilio/rails/phone/twiml/timeout_operation.rb +48 -0
- data/app/operations/twilio/rails/phone/unanswered_call_operation.rb +22 -0
- data/app/operations/twilio/rails/phone/update_operation.rb +26 -0
- data/app/operations/twilio/rails/phone/update_response_operation.rb +38 -0
- data/app/operations/twilio/rails/sms/base_operation.rb +17 -0
- data/app/operations/twilio/rails/sms/create_operation.rb +23 -0
- data/app/operations/twilio/rails/sms/find_message_operation.rb +15 -0
- data/app/operations/twilio/rails/sms/find_operation.rb +15 -0
- data/app/operations/twilio/rails/sms/send_operation.rb +102 -0
- data/app/operations/twilio/rails/sms/twiml/base_operation.rb +11 -0
- data/app/operations/twilio/rails/sms/twiml/error_operation.rb +15 -0
- data/app/operations/twilio/rails/sms/twiml/message_operation.rb +49 -0
- data/app/operations/twilio/rails/sms/update_message_operation.rb +27 -0
- data/app/views/layouts/twilio/rails/application.html.erb +15 -0
- data/config/routes.rb +16 -0
- data/lib/generators/twilio/rails/install/USAGE +15 -0
- data/lib/generators/twilio/rails/install/install_generator.rb +34 -0
- data/lib/generators/twilio/rails/install/templates/initializer.rb +83 -0
- data/lib/generators/twilio/rails/install/templates/message.rb +4 -0
- data/lib/generators/twilio/rails/install/templates/migration.rb +89 -0
- data/lib/generators/twilio/rails/install/templates/phone_call.rb +4 -0
- data/lib/generators/twilio/rails/install/templates/phone_caller.rb +4 -0
- data/lib/generators/twilio/rails/install/templates/recording.rb +4 -0
- data/lib/generators/twilio/rails/install/templates/response.rb +4 -0
- data/lib/generators/twilio/rails/install/templates/sms_conversation.rb +4 -0
- data/lib/generators/twilio/rails/phone_tree/USAGE +8 -0
- data/lib/generators/twilio/rails/phone_tree/phone_tree_generator.rb +12 -0
- data/lib/generators/twilio/rails/phone_tree/templates/tree.rb.erb +13 -0
- data/lib/generators/twilio/rails/sms_responder/USAGE +8 -0
- data/lib/generators/twilio/rails/sms_responder/sms_responder_generator.rb +12 -0
- data/lib/generators/twilio/rails/sms_responder/templates/responder.rb.erb +10 -0
- data/lib/tasks/rails_tasks.rake +45 -0
- data/lib/twilio/rails/client.rb +75 -0
- data/lib/twilio/rails/concerns/has_direction.rb +25 -0
- data/lib/twilio/rails/concerns/has_phone_number.rb +27 -0
- data/lib/twilio/rails/concerns/has_time_scopes.rb +19 -0
- data/lib/twilio/rails/configuration.rb +380 -0
- data/lib/twilio/rails/engine.rb +11 -0
- data/lib/twilio/rails/formatter.rb +93 -0
- data/lib/twilio/rails/models/message.rb +21 -0
- data/lib/twilio/rails/models/phone_call.rb +132 -0
- data/lib/twilio/rails/models/phone_caller.rb +100 -0
- data/lib/twilio/rails/models/recording.rb +27 -0
- data/lib/twilio/rails/models/response.rb +153 -0
- data/lib/twilio/rails/models/sms_conversation.rb +29 -0
- data/lib/twilio/rails/phone/base_tree.rb +229 -0
- data/lib/twilio/rails/phone/tree.rb +229 -0
- data/lib/twilio/rails/phone/tree_macros.rb +147 -0
- data/lib/twilio/rails/phone.rb +12 -0
- data/lib/twilio/rails/phone_number.rb +29 -0
- data/lib/twilio/rails/railtie.rb +17 -0
- data/lib/twilio/rails/sms/delegated_responder.rb +97 -0
- data/lib/twilio/rails/sms/responder.rb +33 -0
- data/lib/twilio/rails/sms.rb +12 -0
- data/lib/twilio/rails/version.rb +5 -0
- data/lib/twilio/rails.rb +89 -0
- metadata +289 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Twilio
|
3
|
+
module Rails
|
4
|
+
module Phone
|
5
|
+
# Performs the {Twilio::Rails::Phone::AttachRecordingOperation}.
|
6
|
+
class AttachRecordingJob < ::Twilio::Rails::ApplicationJob
|
7
|
+
queue_as :default
|
8
|
+
|
9
|
+
def perform(recording_id:)
|
10
|
+
Twilio::Rails::Phone::AttachRecordingOperation.call(recording_id: recording_id)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Twilio
|
3
|
+
module Rails
|
4
|
+
module Phone
|
5
|
+
# Performs the {Twilio::Rails::Phone::FinishedCallOperation}.
|
6
|
+
class FinishedCallJob < ::Twilio::Rails::ApplicationJob
|
7
|
+
queue_as :default
|
8
|
+
|
9
|
+
def perform(phone_call_id:)
|
10
|
+
Twilio::Rails::Phone::FinishedCallOperation.call(phone_call_id: phone_call_id)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Twilio
|
3
|
+
module Rails
|
4
|
+
module Phone
|
5
|
+
# Performs the {Twilio::Rails::Phone::UnansweredCallOperation}.
|
6
|
+
class UnansweredCallJob < ::Twilio::Rails::ApplicationJob
|
7
|
+
queue_as :default
|
8
|
+
|
9
|
+
def perform(phone_call_id:)
|
10
|
+
Twilio::Rails::Phone::UnansweredCallOperation.call(phone_call_id: phone_call_id)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Twilio
|
3
|
+
module Rails
|
4
|
+
class ApplicationOperation < ActiveOperation::Base
|
5
|
+
# Annotates the log output for every operation with the execution time.
|
6
|
+
around do |instance, executable|
|
7
|
+
@operation_runtime_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
8
|
+
executable.call
|
9
|
+
@operation_runtime_stop = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
10
|
+
|
11
|
+
::Twilio::Rails.config.logger.tagged(self.class) { |l| l.info("execution time #{ instance.operation_runtime_seconds } seconds")}
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
def operation_runtime_seconds
|
17
|
+
(@operation_runtime_stop || Process.clock_gettime(Process::CLOCK_MONOTONIC)) - @operation_runtime_start
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Twilio
|
3
|
+
module Rails
|
4
|
+
# Finds and returns a {Twilio::Rails::Models::PhoneCaller} by phone number, or creates a new one if it does not
|
5
|
+
# exist. The operation will `halt` if the phone number is not valid or blank.
|
6
|
+
#
|
7
|
+
# *Note:* Operations should be called with `call(params)` and not by calling `new(params).execute` directly.
|
8
|
+
class FindOrCreatePhoneCallerOperation < ::Twilio::Rails::ApplicationOperation
|
9
|
+
input :phone_number, accepts: String, type: :keyword, required: false
|
10
|
+
|
11
|
+
# @param phone_number [String] The phone number to find or create the phone caller.
|
12
|
+
# @return [Twilio::Rails::Models::PhoneCaller] The found or newly created phone caller.
|
13
|
+
def execute
|
14
|
+
halt nil unless valid_phone_number
|
15
|
+
|
16
|
+
phone_caller = ::Twilio::Rails.config.phone_caller_class.find_or_initialize_by(phone_number: valid_phone_number)
|
17
|
+
phone_caller.save! if phone_caller.new_record?
|
18
|
+
|
19
|
+
phone_caller
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def valid_phone_number
|
25
|
+
Twilio::Rails::Formatter.coerce_to_valid_phone_number(phone_number)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Twilio
|
3
|
+
module Rails
|
4
|
+
module Phone
|
5
|
+
# Called by {Twilio::Rails::Phone::ReceiveRecordingOperation} to download and store the audio file from the URL
|
6
|
+
# provided by Twilio.
|
7
|
+
class AttachRecordingOperation < ::Twilio::Rails::ApplicationOperation
|
8
|
+
input :recording_id, accepts: Integer, type: :keyword, required: true
|
9
|
+
|
10
|
+
def execute
|
11
|
+
recording = ::Twilio::Rails.config.recording_class.find(recording_id)
|
12
|
+
|
13
|
+
if !recording.audio.attached?
|
14
|
+
if recording.url.blank?
|
15
|
+
raise Twilio::Rails::Phone::Error, "[AttachRecordingOperation] Has a blank URL and cannot be fetched recording_id=#{ recording.id }"
|
16
|
+
end
|
17
|
+
|
18
|
+
response = Faraday.get(recording.url)
|
19
|
+
|
20
|
+
if response.success?
|
21
|
+
recording.audio.attach(io: StringIO.new(response.body), filename: "recording.wav", content_type: "audio/wav")
|
22
|
+
recording.save!
|
23
|
+
else
|
24
|
+
raise Twilio::Rails::Phone::Error, "[AttachRecordingOperation] Failed to fetch recording recording_id=#{ recording.id } HTTP#{ response.status } from #{ recording.url }"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Twilio
|
3
|
+
module Rails
|
4
|
+
module Phone
|
5
|
+
# Base class for all SMS operations. Requires the `phone_call_id` to be passed in.
|
6
|
+
class BaseOperation < ::Twilio::Rails::ApplicationOperation
|
7
|
+
input :phone_call_id, accepts: Integer, type: :keyword, required: true
|
8
|
+
|
9
|
+
protected
|
10
|
+
|
11
|
+
def phone_call
|
12
|
+
@phone_call ||= ::Twilio::Rails.config.phone_call_class.find(phone_call_id)
|
13
|
+
end
|
14
|
+
|
15
|
+
def phone_caller
|
16
|
+
phone_call.phone_caller
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Twilio
|
3
|
+
module Rails
|
4
|
+
module Phone
|
5
|
+
class CreateOperation < ::Twilio::Rails::ApplicationOperation
|
6
|
+
input :params, accepts: Hash, type: :keyword, required: true
|
7
|
+
input :tree, accepts: Twilio::Rails::Phone::Tree, type: :keyword, required: true
|
8
|
+
|
9
|
+
def execute
|
10
|
+
phone_call = ::Twilio::Rails.config.phone_call_class.new(
|
11
|
+
sid: params["CallSid"],
|
12
|
+
direction: params["direction"].presence || "inbound",
|
13
|
+
call_status: params["CallStatus"],
|
14
|
+
tree_name: tree.name,
|
15
|
+
number: params["Called"].presence || params["To"].presence,
|
16
|
+
from_number: params["Caller"].presence || params["From"].presence,
|
17
|
+
from_city: params["CallerCity"].presence || params["FromCity"].presence,
|
18
|
+
from_province: params["CallerState"].presence || params["FromState"].presence,
|
19
|
+
from_country: params["CallerCountry"].presence || params["FromCountry"].presence,
|
20
|
+
)
|
21
|
+
|
22
|
+
phone_caller = Twilio::Rails::FindOrCreatePhoneCallerOperation.call(phone_number: phone_call.from_number)
|
23
|
+
|
24
|
+
if !phone_caller
|
25
|
+
error_message = if !Twilio::Rails::Formatter.valid_north_american_phone_number?(phone_call.from_number)
|
26
|
+
"The phone number is invalid."
|
27
|
+
else
|
28
|
+
"The phone caller could not be persisted or retrieved."
|
29
|
+
end
|
30
|
+
|
31
|
+
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
|
+
end
|
33
|
+
|
34
|
+
phone_call.phone_caller = phone_caller
|
35
|
+
phone_call.save!
|
36
|
+
|
37
|
+
phone_call
|
38
|
+
rescue => e
|
39
|
+
Twilio::Rails.notify_exception(e,
|
40
|
+
message: "Failed to handle incoming Twilio phone call.",
|
41
|
+
context: { params: params, tree: tree },
|
42
|
+
exception_binding: binding
|
43
|
+
)
|
44
|
+
raise
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Twilio
|
3
|
+
module Rails
|
4
|
+
module Phone
|
5
|
+
class FindOperation < ::Twilio::Rails::ApplicationOperation
|
6
|
+
input :params, accepts: Hash, type: :keyword, required: true
|
7
|
+
|
8
|
+
def execute
|
9
|
+
::Twilio::Rails.config.phone_call_class.find_by!(sid: params["CallSid"])
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Twilio
|
3
|
+
module Rails
|
4
|
+
module Phone
|
5
|
+
class FinishedCallOperation < ::Twilio::Rails::Phone::BaseOperation
|
6
|
+
def execute
|
7
|
+
if phone_call.finished?
|
8
|
+
Twilio::Rails.config.logger.tagged(self.class) { |l| l.warn("Skipping duplicate finished call job") }
|
9
|
+
else
|
10
|
+
phone_call.update!(finished: true)
|
11
|
+
phone_call.tree.finished_call.call(phone_call) if phone_call.tree.finished_call
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Twilio
|
3
|
+
module Rails
|
4
|
+
module Phone
|
5
|
+
class ReceiveRecordingOperation < ::Twilio::Rails::Phone::BaseOperation
|
6
|
+
input :params, accepts: Hash, type: :keyword, required: true
|
7
|
+
input :response_id, accepts: Integer, type: :keyword, required: true
|
8
|
+
|
9
|
+
def execute
|
10
|
+
response = phone_call.responses.find(response_id)
|
11
|
+
|
12
|
+
if phone_call.recordings.sid(params["RecordingSid"]).any?
|
13
|
+
Twilio::Rails.config.logger.tagged(self.class) { |l| l.warn("duplicate recording for response_id=#{ response.id } recording_sid=#{ params["RecordingSid"] }") }
|
14
|
+
else
|
15
|
+
recording = phone_call.recordings.build(
|
16
|
+
recording_sid: params["RecordingSid"],
|
17
|
+
url: params["RecordingUrl"],
|
18
|
+
duration: params["RecordingDuration"].presence,
|
19
|
+
)
|
20
|
+
recording.save!
|
21
|
+
|
22
|
+
response.recording = recording
|
23
|
+
response.save!
|
24
|
+
|
25
|
+
if Twilio::Rails.config.attach_recording?(recording)
|
26
|
+
Twilio::Rails::Phone::AttachRecordingJob.set(wait: 5.seconds).perform_later(recording_id: recording.id)
|
27
|
+
end
|
28
|
+
|
29
|
+
recording
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Twilio
|
3
|
+
module Rails
|
4
|
+
module Phone
|
5
|
+
class StartCallOperation < ::Twilio::Rails::ApplicationOperation
|
6
|
+
input :tree, accepts: Twilio::Rails::Phone::Tree, type: :keyword, required: true
|
7
|
+
input :to, accepts: String, type: :keyword, required: true
|
8
|
+
input :from, accepts: [String, Twilio::Rails::PhoneNumber], type: :keyword, required: false
|
9
|
+
input :answering_machine_detection, accepts: [true, false], default: true, type: :keyword, required: false
|
10
|
+
|
11
|
+
def execute
|
12
|
+
from = if self.from.is_a?(Twilio::Rails::PhoneNumber)
|
13
|
+
self.from.number
|
14
|
+
elsif self.from.present?
|
15
|
+
self.from
|
16
|
+
else
|
17
|
+
Twilio::Rails.config.default_outgoing_phone_number
|
18
|
+
end
|
19
|
+
|
20
|
+
params = {
|
21
|
+
"CallSid" => nil,
|
22
|
+
"direction" => "outbound",
|
23
|
+
"To" => from,
|
24
|
+
"From" => to,
|
25
|
+
}
|
26
|
+
|
27
|
+
begin
|
28
|
+
sid = Twilio::Rails::Client.start_call(url: tree.outbound_url, to: to, from: from, answering_machine_detection: answering_machine_detection)
|
29
|
+
params["CallSid"] = sid
|
30
|
+
rescue Twilio::REST::TwilioError => e
|
31
|
+
Twilio::Rails.notify_exception(e,
|
32
|
+
message: "Failed to start Twilio phone call. Got REST error response.",
|
33
|
+
context: { params: params },
|
34
|
+
exception_binding: binding
|
35
|
+
)
|
36
|
+
raise
|
37
|
+
rescue => e
|
38
|
+
Twilio::Rails.notify_exception(e,
|
39
|
+
message: "Failed to start Twilio phone call. Got unknown error.",
|
40
|
+
context: { params: params },
|
41
|
+
exception_binding: binding
|
42
|
+
)
|
43
|
+
raise
|
44
|
+
end
|
45
|
+
|
46
|
+
# TODO: I think this may be a race condition
|
47
|
+
phone_call = Twilio::Rails::Phone::CreateOperation.call(params: params, tree: tree)
|
48
|
+
phone_call
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Twilio
|
3
|
+
module Rails
|
4
|
+
module Phone
|
5
|
+
module Twiml
|
6
|
+
class AfterOperation < Twilio::Rails::Phone::Twiml::BaseOperation
|
7
|
+
input :tree, accepts: Twilio::Rails::Phone::Tree, type: :keyword, required: true
|
8
|
+
input :after, accepts: Twilio::Rails::Phone::Tree::After, type: :keyword, required: true
|
9
|
+
|
10
|
+
def execute
|
11
|
+
unless after.hangup?
|
12
|
+
next_response = phone_call.responses.build(prompt_handle: after.prompt)
|
13
|
+
next_response.save!
|
14
|
+
end
|
15
|
+
|
16
|
+
twiml_response = Twilio::TwiML::VoiceResponse.new do |twiml|
|
17
|
+
add_messages(twiml, message_set: after.messages, response: next_response)
|
18
|
+
|
19
|
+
if after.hangup?
|
20
|
+
twiml.hangup
|
21
|
+
else
|
22
|
+
twiml.redirect(::Twilio::Rails::Engine.routes.url_helpers.phone_prompt_path(
|
23
|
+
format: :xml,
|
24
|
+
tree_name: tree.name,
|
25
|
+
response_id: next_response.id
|
26
|
+
))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
Twilio::Rails.config.logger.info("after_twiml: #{twiml_response.to_s}")
|
31
|
+
twiml_response.to_s
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Twilio
|
3
|
+
module Rails
|
4
|
+
module Phone
|
5
|
+
module Twiml
|
6
|
+
class BaseOperation < Twilio::Rails::Phone::BaseOperation
|
7
|
+
protected
|
8
|
+
|
9
|
+
# Adds messages to the TwiML response from the passed in message set. This mutates the passed in TwiML object,
|
10
|
+
# which isn't ideal.
|
11
|
+
#
|
12
|
+
# @param twiml [Twilio::TwiML::VoiceResponse] the TwiML response object to add messages to.
|
13
|
+
# @param message_set [Twilio::Rails::Phone::Tree::MessageSet, Hash, Proc, Array, String] the message or messages to add to the TwiML response.
|
14
|
+
# @param response [Twilio::Rails::Phone::Response] the response passed to the proc when they are called.
|
15
|
+
def add_messages(twiml, message_set:, response:)
|
16
|
+
message_set = message_set.call(response) if message_set.is_a?(Proc)
|
17
|
+
message_set = Twilio::Rails::Phone::Tree::MessageSet.new(message: message_set) unless message_set.is_a?(Twilio::Rails::Phone::Tree::MessageSet)
|
18
|
+
|
19
|
+
message_set.each do |message|
|
20
|
+
message = message.call(response) if message.is_a?(Proc)
|
21
|
+
message = Twilio::Rails::Phone::Tree::Message.new(**message) if message.is_a?(Hash)
|
22
|
+
next if message.blank?
|
23
|
+
message = Twilio::Rails::Phone::Tree::Message.new(say: message, voice: tree.config[:voice]) if message.is_a?(String)
|
24
|
+
|
25
|
+
raise Twilio::Rails::Phone::InvalidTreeError "unknown message #{ message } is a #{ message.class }" unless message.is_a?(Twilio::Rails::Phone::Tree::Message)
|
26
|
+
|
27
|
+
# TODO: if we want to make a transcript of sent messages this is where we would record outgoing messages
|
28
|
+
|
29
|
+
if message.say?
|
30
|
+
value = message.value
|
31
|
+
value = message.value.call(response) if message.value.is_a?(Proc)
|
32
|
+
twiml.say(voice: message.voice || tree.config[:voice], message: value) do |say|
|
33
|
+
message.block.call(say) if message.block
|
34
|
+
end
|
35
|
+
elsif message.play?
|
36
|
+
value = message.value
|
37
|
+
value = message.value.call(response) if message.value.is_a?(Proc)
|
38
|
+
twiml.play(url: value)
|
39
|
+
elsif message.pause?
|
40
|
+
twiml.pause(length: message.value)
|
41
|
+
else
|
42
|
+
raise Twilio::Rails::Phone::InvalidTreeError, "unknown message #{ message }"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Twilio
|
3
|
+
module Rails
|
4
|
+
module Phone
|
5
|
+
module Twiml
|
6
|
+
class ErrorOperation < Twilio::Rails::Phone::Twiml::BaseOperation
|
7
|
+
input :tree, accepts: Twilio::Rails::Phone::Tree, type: :keyword, required: true
|
8
|
+
input :messages, accepts: Object, type: :keyword, required: false
|
9
|
+
|
10
|
+
def execute
|
11
|
+
twiml = Twilio::TwiML::VoiceResponse.new
|
12
|
+
add_messages(twiml, message_set: messages, response: phone_call.responses.build) if messages
|
13
|
+
twiml.hangup
|
14
|
+
|
15
|
+
Twilio::Rails.config.logger.info("error_twiml: #{twiml.to_s}")
|
16
|
+
twiml.to_s
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Twilio
|
3
|
+
module Rails
|
4
|
+
module Phone
|
5
|
+
module Twiml
|
6
|
+
class GreetingOperation < Twilio::Rails::Phone::Twiml::BaseOperation
|
7
|
+
input :tree, accepts: Twilio::Rails::Phone::Tree, type: :keyword, required: true
|
8
|
+
|
9
|
+
def execute
|
10
|
+
if !phone_caller.valid_north_american_phone_number? && tree.config[:invalid_phone_number]
|
11
|
+
Twilio::Rails::Phone::Twiml::ErrorOperation.call(phone_call_id: phone_call.id, tree: tree, messages: tree.config[:invalid_phone_number])
|
12
|
+
else
|
13
|
+
after = tree.greeting
|
14
|
+
after = Twilio::Rails::Phone::Tree::After.new(after.proc.call(phone_call.responses.build)) if after.proc
|
15
|
+
Twilio::Rails::Phone::Twiml::AfterOperation.call(phone_call_id: phone_call.id, tree: tree, after: after)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Twilio
|
3
|
+
module Rails
|
4
|
+
module Phone
|
5
|
+
module Twiml
|
6
|
+
class PromptOperation < Twilio::Rails::Phone::Twiml::BaseOperation
|
7
|
+
input :tree, accepts: Twilio::Rails::Phone::Tree, type: :keyword, required: true
|
8
|
+
input :response_id, accepts: Integer, type: :keyword, required: true
|
9
|
+
|
10
|
+
def execute
|
11
|
+
return Twilio::Rails::Phone::Twiml::ErrorOperation.call(phone_call_id: phone_call.id, tree: tree) if phone_call.answering_machine?
|
12
|
+
|
13
|
+
response = phone_call.responses.find(response_id)
|
14
|
+
prompt = tree.prompts[response.prompt_handle]
|
15
|
+
raise Twilio::Rails::Phone::InvalidTreeError, "cannot find #{ response.prompt_handle } in #{ tree.name }" unless prompt
|
16
|
+
|
17
|
+
twiml_response = Twilio::TwiML::VoiceResponse.new do |twiml|
|
18
|
+
unless prompt.gather&.interrupt?
|
19
|
+
add_messages(twiml, message_set: prompt.messages, response: response)
|
20
|
+
end
|
21
|
+
|
22
|
+
case prompt.gather&.type
|
23
|
+
when :digits
|
24
|
+
args = {
|
25
|
+
action: ::Twilio::Rails::Engine.routes.url_helpers.phone_prompt_response_path(
|
26
|
+
format: :xml,
|
27
|
+
tree_name: tree.name,
|
28
|
+
response_id: response.id
|
29
|
+
),
|
30
|
+
input: "dtmf",
|
31
|
+
num_digits: prompt.gather.args[:number],
|
32
|
+
timeout: prompt.gather.args[:timeout],
|
33
|
+
action_on_empty_result: false,
|
34
|
+
}
|
35
|
+
|
36
|
+
args[:finish_on_key] = prompt.gather.args[:finish_on_key] if prompt.gather.args[:finish_on_key]
|
37
|
+
|
38
|
+
twiml.gather(**args) do |twiml|
|
39
|
+
if prompt.gather&.interrupt?
|
40
|
+
add_messages(twiml, message_set: prompt.messages, response: response)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
twiml.redirect(::Twilio::Rails::Engine.routes.url_helpers.phone_timeout_path(
|
44
|
+
format: :xml,
|
45
|
+
tree_name: tree.name,
|
46
|
+
response_id: response.id
|
47
|
+
))
|
48
|
+
when :voice
|
49
|
+
args = {
|
50
|
+
max_length: prompt.gather.args[:length],
|
51
|
+
play_beep: prompt.gather.args[:beep],
|
52
|
+
# trim: "trim-silence",
|
53
|
+
timeout: prompt.gather.args[:timeout],
|
54
|
+
action: ::Twilio::Rails::Engine.routes.url_helpers.phone_prompt_response_path(
|
55
|
+
format: :xml,
|
56
|
+
tree_name: tree.name,
|
57
|
+
response_id: response.id
|
58
|
+
),
|
59
|
+
recording_status_callback: ::Twilio::Rails::Engine.routes.url_helpers.phone_receive_recording_path(
|
60
|
+
response_id: response.id
|
61
|
+
),
|
62
|
+
}
|
63
|
+
|
64
|
+
if prompt.gather.args[:transcribe]
|
65
|
+
args[:transcribe] = true
|
66
|
+
args[:transcribe_callback] = ::Twilio::Rails::Engine.routes.url_helpers.phone_transcribe_path(response_id: response.id)
|
67
|
+
end
|
68
|
+
|
69
|
+
args[:profanity_filter] = true if prompt.gather.args[:profanity_filter]
|
70
|
+
|
71
|
+
twiml.record(**args)
|
72
|
+
when :speech
|
73
|
+
args = {
|
74
|
+
action: ::Twilio::Rails::Engine.routes.url_helpers.phone_prompt_response_path(
|
75
|
+
format: :xml,
|
76
|
+
tree_name: tree.name,
|
77
|
+
response_id: response.id
|
78
|
+
),
|
79
|
+
input: "speech",
|
80
|
+
timeout: prompt.gather.args[:timeout],
|
81
|
+
action_on_empty_result: true,
|
82
|
+
language: prompt.gather.args[:language].presence || "en-US",
|
83
|
+
enhanced: !!prompt.gather.args[:enhanced],
|
84
|
+
}
|
85
|
+
|
86
|
+
args[:speech_timeout] = prompt.gather.args[:speech_timeout] if prompt.gather.args[:speech_timeout]
|
87
|
+
args[:speech_model] = prompt.gather.args[:speech_model] if prompt.gather.args[:speech_model].present?
|
88
|
+
args[:profanity_filter] = true if prompt.gather.args[:profanity_filter]
|
89
|
+
|
90
|
+
twiml.gather(**args)
|
91
|
+
when nil
|
92
|
+
twiml.redirect(::Twilio::Rails::Engine.routes.url_helpers.phone_prompt_response_path(
|
93
|
+
format: :xml,
|
94
|
+
tree_name: tree.name,
|
95
|
+
response_id: response.id
|
96
|
+
))
|
97
|
+
else
|
98
|
+
raise Twilio::Rails::Phone::InvalidTreeError, "unknown gather type #{prompt.gather.type.inspect}"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
Twilio::Rails.config.logger.info("prompt_twiml: #{twiml_response.to_s}")
|
103
|
+
twiml_response.to_s
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Twilio
|
3
|
+
module Rails
|
4
|
+
module Phone
|
5
|
+
module Twiml
|
6
|
+
class PromptResponseOperation < Twilio::Rails::Phone::Twiml::BaseOperation
|
7
|
+
input :tree, accepts: Twilio::Rails::Phone::Tree, type: :keyword, required: true
|
8
|
+
input :response_id, accepts: Integer, type: :keyword, required: true
|
9
|
+
input :params, accepts: Hash, type: :keyword, required: true
|
10
|
+
|
11
|
+
def execute
|
12
|
+
return Twilio::Rails::Phone::Twiml::ErrorOperation.call(phone_call_id: phone_call.id, tree: tree) if phone_call.answering_machine?
|
13
|
+
|
14
|
+
response = phone_call.responses.find(response_id)
|
15
|
+
response = Twilio::Rails::Phone::UpdateResponseOperation.call(params: params, response_id: response.id, phone_call_id: phone_call.id)
|
16
|
+
|
17
|
+
prompt = tree.prompts[response.prompt_handle]
|
18
|
+
raise Twilio::Rails::Phone::InvalidTreeError, "cannot find #{ response.prompt_handle } in #{ tree.name }" unless prompt
|
19
|
+
|
20
|
+
after = prompt.after
|
21
|
+
after = Twilio::Rails::Phone::Tree::After.new(after.proc.call(response)) if after.proc
|
22
|
+
|
23
|
+
Twilio::Rails::Phone::Twiml::AfterOperation.call(phone_call_id: phone_call.id, tree: tree, after: after)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Twilio
|
3
|
+
module Rails
|
4
|
+
module Phone
|
5
|
+
module Twiml
|
6
|
+
class RequestValidationFailureOperation < ::Twilio::Rails::ApplicationOperation
|
7
|
+
def execute
|
8
|
+
twiml = Twilio::TwiML::VoiceResponse.new
|
9
|
+
twiml.hangup
|
10
|
+
twiml.to_s
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|