With the addition of Call Controllers to Adhearsion 2.0, Adhearsion applications become closer to real MVC applications. A CallController is the "controller" in MVC terms; the call object is the "view" (being the method of interaction between the human and the application, it qualifies here, even though it is not actually visible); models may be any backend you prefer, be they backed by a database, a directory (like LDAP) or anything else. Indeed, one might wish to make use of a second view, such as an XMPP interaction or some kind of push-based rendering to a visual display.
So, how does one write an application based on call controllers? By generating a controller class and routing calls to it as described in Routing. That might look something like this:
$ ahn generate controller SuperSecretProjectCallController exist lib exist spec create lib/super_secret_project_call_controller.rb create spec/super_secret_project_call_controller_spec.rb
config/adhearsion.rb:
Adhearsion.routes do
route 'default', SuperSecretProjectCallController
end
lib/supersecretprojectcallcontroller.rb:
# encoding: utf-8
class SuperSecretProjectCallController < Adhearsion::CallController
def run
end
end
Here, the controller class itself lives in the lib directory, which, by default, is auto-loaded by Adhearsion. You may configure this like so:
config/adhearsion.rb:
Adhearsion.config do |config|
config.lib = 'application/call_controllers'
end
Within a call controller, you have access to many instance methods with which to operate on the call, and you also have access to the call object (#call
) itself.
The entry point for every CallController is the #run
method, and every CallController must have a #run
method. Once launched, there are several methods available to exercise basic control over a call. The first interesting one is the method by which you answer a call. Hold on to your hats here, this is highly complex:
class SuperSecretProjectCallController < Adhearsion::CallController
def run
answer
end
end
Hanging up a call is equally hard work:
class SuperSecretProjectCallController < Adhearsion::CallController
def run
hangup
end
end
You're beginning to see now why telephony consulting is so profitable!
It is also possible to mute and unmute the call:
class SuperSecretProjectCallController < Adhearsion::CallController
def run
answer
mute
unmute
hangup
end
end
One other handy trick is rejecting a call before it is answered. Among other things this avoids billing in most cases:
class SuperSecretProjectCallController < Adhearsion::CallController
def run
reject if call.from =~ /18005551234/ # Ugh, Aunt Sally always talks my ear off...
# Otherwise...
answer
play "musiconhold.mp3"
hangup
end
end
CallController methods will not explictly answer calls to allow the developer to take advantage of early media. EM is a way to send audio to the caller and receive DTMF without actually "raising the phone". In case of normal time-based phone billing, that will result in the call not being billed until it is actually answered.
For example, this could be used to play some pricing information to a caller, or to send custom audio as a ringing tone.
Early media could not work with all methods and channels, so the recommendation is to always use #answer
first unless you know your application does not need that.
It is possible to reuse and share functionality among call controllers. There are two approaches to this. The first is to invoke another controller within the current controller:
class SuperSecretProjectCallController < Adhearsion::CallController
def run
invoke OtherController
# After OtherController the call will continue:
say "Thanks for using OtherController."
end
end
In this case, the new controller is prepared and executed. When it finishes, control is returned to the original controller.
Sometimes, it is desireable to abandon the current controller, and hop to a new one entirely.
class SuperSecretProjectCallController < Adhearsion::CallController
def run
pass OtherController
raise "Inconcievable!" # This code is never reached
end
end
There are several methods by which to render audible output to the call. The full complement can be found in the Output API documentation, but the main methods are described below:
#play
allows for rendering output of several types, in a manner appropriate to that type. It will detect Date/Time objects, numbers, and paths/URLs to audio files. Additionally, it supports rendering collections of such objects in series. As always, further details can be found in the API documentation, but here are some examples:
class MyController < Adhearsion::CallController
def run
answer
play 'file://rub-dub-dub.mp3', 3, 'file://men-in-a-tub.mp3'
play 'file://the-time-is-now.mp3', Time.now
end
end
IMPORTANT: Notes on audio file URLs
Fetching the audio file and playing it back is the job of the telephony engine (or, in some cases, of the media server when using SSML documents that contain both audio and text). This is important because when referencing a file:///
URL it needs to be visible from the telephony engine (Asterisk or FreeSWITCH). Additionally:
* Asterisk only supports file:///
URLs for playback
* FreeSWITCH supports both http:///
and file:///
URLs for playback
In many circumstances, it is desireable to speak certain output using a text-to-speech engine. This is simple using the #say
method, providing either a simple string or an SSML document:
class MyController < Adhearsion::CallController
def run
answer
say "Rub dub dub, three men in a tub."
doc = RubySpeech::SSML.draw do
string "The time is now"
say_as interpret_as: 'time' do
Time.now
end
end
say doc
end
end
As usual, check out the #say API documentation for more.
If a text-to-speech engine is configured, all output will be rendered by the engine; otherwise, only file playback will be supported on Asterisk. The adhearsion-asterisk plugin provides helpers for Asterisk's built in complex-data rendering methods, and its documentation is the appropriate place to find documentation for those.
We will not cover setting up appswift itself here, but configuring Adhearsion to use appswift is simple:
Adhearsion.config do |config|
config.platform.media.default_renderer = :swift
end
Once you do this, all audio output, including audio file playback, will be delegated to app_swift. If you wish to combine this with asterisk native playback, you should use the helpers in adhearsion-asterisk.
It is possible to render output documents via an engine attached to Asterisk via MRCP. You should see the UniMRCP site for details on the Asterisk configuration, and again, the Adhearsion configuration is simple:
Adhearsion.config do |config|
config.platform.media.default_renderer = :unimrcp
end
Phone calls are fairly boring if they only involve listening to output, so Adhearsion includes several ways to gather input from the caller.
CallController provides the #ask
method, which simplifies input by combining a prompt (rendered as above) with gathering a response.
class MyController < Adhearsion::CallController
def run
answer
result = ask "How many woodchucks? Enter a number followed by #.", terminator: '#'
say "Wow, #{result.response} is a lot of woodchucks!"
end
end
Here, we choose to cease input using a terminator digit. Alternative strategies include a :timeout
, or a digit :limit
, which are described in the #ask API documentation. Additionally, it is possible to pass a block, to which #ask
will yield the digit buffer after each digit is received, in order to validate the input and optionally terminate early. If the block returns a truthy value when invoked, the input will be terminated early.
Rapid and painless creation of complex IVRs has always been one of the defining features of Adhearsion for beginning and advanced programmers alike. Through the #menu
DSL method, the framework abstracts and packages the output and input management and the complex state machine needed to implement a complete menu with audio prompts, digit checking, retries and failure handling, making creating menus a breeze.
A sample menu might look something like this:
class MyController < Adhearsion::CallController
def run
answer
menu "Where can we take you today?", timeout: 8.seconds, tries: 3 do
match 1, BooController
match '2', MyOtherController
match(3, 4) { pass YetAnotherController }
match 5, FooController
match 6..10 do |dialed|
say_dialed dialed
end
timeout { do_this_on_timeout }
invalid do
invoke InvalidController
end
failure do
speak 'Goodbye'
hangup
end
end
speak "This code gets executed unless #pass is used"
end
def say_dialed(dialed)
speak "#{dialed} was dialed"
end
def do_this_on_timeout
speak 'Timeout'
end
end
The first arguments to #menu
are a list of sounds to play, as accepted by #play
, including strings for TTS, Date and Time objects, and file paths. Sounds will be played at the beginning of the menu and after each timeout or invalid input, if the maximum number of tries has not been reached yet.
The :tries
and :timeout
options respectively specify the number of tries before going into failure, and the timeout in seconds allowed before the first and each subsequent digit input.
The most important section is the following block, which specifies how the menu will be constructed and handled.
The #match
method takes an Integer
, a String
, a Range
or any number of them as the required input(s) for the match payload to be executed. The last argument to a #match
is either the name of a CallController, which will be invoked, or a block to be executed. Matched input is passed in to the associated block, or to the controller through its metadata (Controller#metadata[:extension]
).
#menu
executes the payload for the first exact unambiguous match it finds after each input or timing out. In a situation where there might be overlapping patterns, such as 10 and 100, #menu
will wait for timeout after the second digit.
#timeout
, #invalid
and #failure
are for handling bad or missing inputs. These methods only accept blocks as payload, but it is still possible to make use of another CallController by using #pass
or #invoke
within the block.
#invalid
has its associated block executed when the input does not possibly match any pattern.#timeout
block is run when time expires before or between input digits, without there being at least one exact match.#failure
runs its block when the maximum number of tries is reached without an input match.Execution of the current context resumes after #menu
finishes. If you wish to jump to an entirely different controller, #pass
can be used.
#menu
will return a CallController::Input::Result object detailing the success or otherwise of the menu, similarly to #ask
.
The #record
method provides the ability to capture audio in a blocking or non-blocking way.
In this case, it is desireable for #record to block until the recording completes, as it is the main feature of that part of the call. This usage is simple:
class SuperSecretProjectCall < Adhearsion::CallController
def run
answer
record_result = record start_beep: true, max_duration: 60_000
logger.info "Recording saved to #{record_result.recording_uri}"
say "Goodbye!"
end
end
Alternatively, it may be desireable to record a call in the background while other interactions occurr, perhaps for training, fact-verification or compliance reasons. In this case, it is necessary to execute the recording asynchronously, and handle its recording as a callback receiving the Event object for the command:
class SuperSecretProjectCall < Adhearsion::CallController
def run
answer
record async: true do |event|
logger.info "Async recording saved to #{event.recording.uri}"
end
say "We are currently recording this call"
hangup # Triggers the end of the recording
end
end
Adhearsion will check that the recording directory it uses, /var/punchblock/record
, is in place when starting up on an Asterisk platform. A warning will be issued if the directory is missing. If Adhearsion and Asterisk are not running on the same machine, the warning can be disregarded, even though the directory has still to be present on the machine running Asterisk.
It is additionally necessary to have sox installed on the Asterisk machine, because it provides mixing for recordings.
If there are multiple calls active in Adhearsion, it is possible to join the media streams of those calls together so the parties may talk.
class SuperSecretProjectCall < Adhearsion::CallController
def run
answer
join some_other_call # or some_other_call_id
say "We hope you had a nice chat!"
end
end
Here, #join
will block until either the 3rd-party call hangs up, or is otherwise unjoined out-of-band. It is possible to do an asynchronous join by specifying async: true
, in which case #join
will only block until the join is confirmed. Your controller will then resume executing commands on the call while the parties are talking.
Calls may be unjoined out-of-band using the Call#unjoin
method, which has a similar signature to #join
.
Similarly to #join
, it is possible to make an outbound call to a third-party, and then to join the calls on answering. The #dial
method handles this automatically.
class SuperSecretProjectCall < Adhearsion::CallController
def run
answer
dial 'sip:foo@bar.com'
say "We hope you had a nice chat!"
end
end
It is also possible to make multiple calls in paralell, under a first-to-answer-wins policy:
class SuperSecretProjectCall < Adhearsion::CallController
def run
answer
dial 'sip:foo@bar.com', 'tel:+1404....'
say "We hope you had a nice chat!"
end
end
It is additionally possible to set a timeout on attempting to dial out, and to check the status of the dial after the fact:
class SuperSecretProjectCall < Adhearsion::CallController
def run
answer
status = dial 'sip:foo@bar.com', for: 30.seconds
case status.result
when :answer
say "We hope you had a nice chat!"
when :error, :timeout
say "Unfortunately we couldn't get hold of Bob."
when :no_answer
say "It looks like Bob didn't want to talk to you."
end
end
end
By default, the outbound calls will have a caller ID matching that of the party which called in to the original controller. Overriding the caller ID for the outbound call is accomplished with the :from
option passed to dial.
class SuperSecretProjectCall < Adhearsion::CallController
def run
answer
dial 'sip:foo@bar.com', from: 'sip:me@here.com'
say "We hope you had a nice chat!"
end
end
If you wish to perform some action on the outbound call prior to joining, you may pass a controller class with the option key :confirm
like so:
class ConfirmationController < Adhearsion::CallController
def run
say "Incoming call from #{call.from}. Hang up now to reject the call."
end
end
class SuperSecretProjectCall < Adhearsion::CallController
def run
answer
dial 'sip:foo@bar.com', confirm: ConfirmationController
say "We hope you had a nice chat!"
end
end
Further details can be found in the #dial API documentation.
An outbound call may be created and sent to a new CallController instead of joining to an existing call. In this case it is possible to have an inbound call, or event, trigger a new call that plays a message, allows a user to be prompted for input before further action and more.
When the outbound call is placed in an existing CallController that new call is routed to a new CallController via Adhearsion configuration just like an inbound call.
# config/adhearsion.rb
Adhearsion.config do |config|
Adhearsion.router do
route 'Outbound Notification', OutboundNotification, Adhearsion::OutboundCall
route 'Inbound Call', InboundProjectCall
end
end
# Call Controllers
class InboundProjectCall < Adhearsion::CallController
def run
answer
say "Thank you for calling, we will notify Jason that you called."
hangup
Adhearsion::OutboundCall.originate 'sip:jason@adhearsion.com', from: 'sip:foo@bar.com'
end
end
class OutboundNotification < Adhearsion::CallController
def run
answer
say "Foo called!"
hangup
end
end
Alternatively, it is possible to specify a controller for the call to execute when it is answered. You can specify either a controller class:
class InboundCall < Adhearsion::CallController
def run
answer
say "Thank you for calling, we will notify Jason that you called."
hangup
Adhearsion::OutboundCall.originate 'sip:jason@adhearsion.com', from: 'sip:foo@bar.com', controller: OutboundNotification
end
end
class OutboundNotification < Adhearsion::CallController
def run
answer
say "Foo called!"
hangup
end
end
or pass a block to be executed as a controller:
class InboundCall < Adhearsion::CallController
def run
answer
say "Thank you for calling, we will notify Jason that you called."
hangup
Adhearsion::OutboundCall.originate 'sip:jason@adhearsion.com', from: 'sip:foo@bar.com' do
answer
say "Foo called!"
hangup
end
end
end
Further details can be found in the #originate API documentation.
Finally, it is possible to define callbacks to be executed at various stages of a call, or in response to certain events. These are before_call
and after_call
, they are class methods, and they take either a block or a symbol (called as an instance method) like so:
lib/supersecretproject_call.rb:
class SuperSecretProjectCall < Adhearsion::CallController
before_call do
@user = User.find_by_phone_number call.from
end
after_call :save_user
def run
answer
pin = collect_pin
...
end
def collect_pin
input 4
end
def save_user
@user.save
end
end