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"}]
endRun mix deps.get to install the package. Then, add :telephonist to your
applications list. For example:
def application do
[mod: {YourApp, []},
applications: [:logger, :telephonist]]
endThis 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
endThe 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
}
endThe 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!"
endTransitions 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
endNote 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)
endControl 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
endThis 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
endThe 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
endThat'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/3macro.
Telephonist is under the MIT license. See the LICENSE file for more details.