Telephonist makes it easy to design state machines for Twilio calls. These state machines bring TwiML and logic together in one place, making call flows easier to maintain.
Get it from Hex by adding it to your deps
in mix.exs
:
def deps do
[{:telephonist, "~> 0.1.2"}]
end
Run mix deps.get
to install the package. Then, add :telephonist
to your
applications list. For example:
def application do
[mod: {YourApp, []},
applications: [:logger, :telephonist]]
end
This will ensure that all Telephonist's processes are started and supervised properly.
Like most state machines, Telephonist state machines are based on two concepts: state and transitions.
A state is represented by the Telephonist.State
struct.
%Telephonist.State{
machine: MachineName,
name: :state_name,
options: [],
twiml: "<?xml ..."
}
States are primarily used to define what TwiML should be displayed to Twilio for
a given call at a particular time. Telephonist provides a simple macro to make
generating and returning Telephonist.State
structs easy:
defmodule CustomStateMachine do
use Telephonist.StateMachine, initial_state: :introduction
state :introduction, _twilio, _options do
say "Welcome to my phone tree!"
end
end
The state/3
macro is just sugar, and defines a function like this:
def state(:introduction, _twilio, options) do
xml = twiml do
say "Welcome to my phone tree!"
end
%Telephonist.State{
machine: __MODULE__,
name: :introduction,
options: options,
twiml: xml
}
end
The three arguments are as follows:
state_name
: the name of the state, obviously.twilio
: a map of parameters sent in from Twilio.options
: a map of custom options defined by you at various points during the call's lifecycle.
Whenever Telephonist wants to get a particular state out of your module, it will
call the state/3
function generated by the state/3
macro, like so:
# twilio -> a map of parameters that came from Twilio
# options -> any custom options that are appended to the call over time
CustomStateMachine.state(:introduction, twilio, options)
You can pattern match with the state/3
struct just like a function definition.
state :introduction, _twilio, %{error: msg} do
say "An error occurred! #{msg}"
end
state :introduction, _twilio, _options do
say "Welcome to my phone tree!"
end
Transitions are handled through the transition/3
function. It takes the same
three arguments as the state/3
function or macro.
state_name
: the name of the state that is being transitioned from.twilio
: a map of parameters passed in from Twilio.options
: a map of custom parameters defined by you.
You can define it on your state machines like so:
defmodule CustomCallFlow do
use Telephonist.StateMachine, initial_state: :choose_language
state :choose_language, twilio, options do
say "#{options[:error]}" # say any error, if present
gather timeout: 10 do
say "For English, press 1"
say "Para español, presione 2"
end
end
state :english, twilio, options do
say "Proceeding in English..."
end
state :spanish, twilio, options do
say "Procediendo en español..."
end
# If the user pressed "1" on their keypad, transition to English state
def transition(:choose_language, %{Digits: "1"} = twilio, options) do
state :english, twilio, options
end
# If the user pressed "2" on their keypad, transition to Spanish state
def transition(:choose_language, %{Digits: "2"} = twilio, options) do
state :spanish, twilio, options
end
# If neither of the above are true, append an error to the options and
# remain on the current state
def transition(:choose_language, twilio, options) do
options = Map.put(options, :error, "You pressed an invalid digit. Please try again.")
state :choose_language, twilio, options
end
end
Note that transition/3
must return a Telephonist.State
. This is easily done
by simply calling the state/3
function. Also, note that you can easily switch
to another state machine by simply calling state
on it:
def transition(:choose_language, %{Digits: "1"} = twilio, options) do
EnglishCallFlow.state(:introduction, twilio, options)
end
def transition(:choose_language, %{Digits: "2"} = twilio, options) do
SpanishCallFlow.state(:introduction, twilio, options)
end
Control of the call will then be passed to the other state machine. This allows you to keep your state machines small, focused, and potentially reusable.
When a call completes, Telephonist will call the on_complete/3
callback. It
will receive the Telephonist.State
of the call at the time it completed,
Twilio's final request parameters, and the custom options the call accumulated
during its life:
def on_complete({sid, twilio_call_status, state}, twilio, options) do
:ok
end
This is a good place to put any cleanup logic that you need to perform after a call completes.
This callback will be run if a transition fails due to an exception. This will most often occur when you fail to define a transition or state, or if your pattern matching left a case out. It provides you an opportunity to recover the call and prevent the user from hearing a Twilio error message.
def on_transition_error(exception, state_name, twilio, options) do
# To prevent an error, return a new state:
state :recover, twilio, options
end
The default implementation of on_transition_error/4
that comes with
Telephonist.StateMachine
will simply re-raise the error.
Once you've defined a state machine, you can process calls through it using
Telephonist.CallProcessor
.
# The web framework shown here is pseudo-code
def index(conn, twilio) do
options = %{} # Whatever I want to be able to use in my states and transitions
state = Telephonist.CallProcessor.process(MyStateMachine, twilio, options)
render conn, xml: state.twiml
end
That's it! New calls will start off in MyStateMachine.initial_state
and
progress from there.
Telephonist publishes events via GenEvent
. In fact, Telephonist.Logger
is
simply a subscriber to these events. Look there for an example of how to
implement your own subscriber.
See these other Elixir libraries I've written for Elixir:
- ExTwilio. A Twilio API client.
- ExTwiml. Render TwiML from Elixir. This is actually a dependency
of Telephonist, and is used in the
state/3
macro.
Telephonist is under the MIT license. See the LICENSE file for more details.