Elixir state machines for Twilio calls
Elixir Ruby
Switch branches/tags
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
config
lib
script
test
.gitignore
.kbignore
.travis.yml
CHANGELOG.md
LICENSE
NOTICE
README.md
SIGNED.md
mix.exs
mix.lock

README.md

Telephonist

Hex Version Build Status Inline docs Deps Status

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.

Installation

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.

Usage

Basic Concepts

Like most state machines, Telephonist state machines are based on two concepts: state and transitions.

State

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

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.

on_complete/3

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.

on_transition_error/4

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.

Processing Calls

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.

Subscribing to Events

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.

Other Twilio Libraries

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.

LICENSE

Telephonist is under the MIT license. See the LICENSE file for more details.