Call Forwarding Application Migration from TwiML to FlexML
If you have read the Migrating from Twilio to CarrierX Quick Start, you could see that TwiML and FlexML are not too much different. Both offer a special syntax to provide instructions, and all you need is to change the representation of these instructions in your code.
But when it comes to a real-case migration from Twilio to CarrierX, users might meet some difficulties.
Let’s see such a migration in details and learn how to solve the issues that arise so that your migrated application worked with CarrierX flawlessly.
Getting Application Source Code
We take the Call Forwarding application from Twilio as an example. This sample application connects callers to the offices of their U.S. senators. You can download the application source code at GitHub.
Once you download the application, you can see that it has the following structure:
[call_forward_flask]
[migrations]
[tests]
.gitignore
.mergify.yml
.travis.yml
LICENSE
README.md
black.toml
free-zipcode-database.csv
manage.py
requirements-to-freeze.txt
requirements.txt
senators.json
setup.cfg
The call_forward_flask
folder holds the files we are going to modify. Here is its structure:
[static]
[templates]
__init__.py
config.py
models.py
parsers.py
views.py
Actually, the only file we need to modify here is views.py
. All the routes used to send requests and responses for our application are here.
Modifying Routes
The views.py
file contains the following routes:
- hello is the basic route that the application uses as a placeholder to the requests sent to the website root.
- callcongress is the main route that the application uses for incoming calls and collecting the information about the state from which the caller originates.
- state_lookup is the route where the application looks up for the state from the entered ZIP code.
- collect_zip is the route that the application uses for collecting the ZIP information if the initial state guess is wrong.
- set_state is the route where the application uses to set the state for the senators selection.
- call_senators is the route for connecting caller to both of their senators.
- call_second_senator is the route that the application uses to forward the caller to their second senator.
- end_call is the route that the application uses to thank user and hang up.
Let’s take a look at each of the routes and see what, where, and how should be modified.
I. hello Route
The hello route does not contain any TwiML specific code, so we can leave it as is:
@app.route('/')
def hello():
return render_template('index.html')
II. callcongress Route
We modify the callcongress route like this:
-
The callcongress route uses the
VoiceResponse()
class to build the TwiML response to the call. In FlexML we do not need it, so we can remove this line. -
The next code portion to change is the way the application gets the data from the call. Twilio sends the call data in the form of an immutable
MultiDict
. In CarrierX, it is pure JSON, which you can receive and parse using the common Flaskrequest
module. Refer to the code below to see how we replace the definition of thefrom_state
variable. -
The
if
statement in TwiML forms a response with the Gather verb. In FlexML we need to change the response to plain FlexML syntax. -
The
else
statement in TwiML also forms a response with theGather
verb but with other parameters. We also change it to FlexML syntax. -
Before returning the response, TwiML needs to form the final response code using the
append()
function. In FlexML we do not need it, so we remove this line. -
Finally, we return the resulting response to the call with the
return
statement. We do not need any additional parameters for this in FlexML.
TwiML Python Code
@app.route('/callcongress/welcome', methods=['POST'])
def callcongress():
response = VoiceResponse()
from_state = request.values.get('FromState', None)
if from_state:
gather = Gather(
num_digits=1,
action='/callcongress/set-state',
method='POST',
from_state=from_state,
)
gather.say(
"Thank you for calling congress! It looks like "
+ "you\'re calling from {}. ".format(from_state)
+ "If this is correct, please press 1. Press 2 if "
+ "this is not your current state of residence."
)
else:
gather = Gather(num_digits=5, action='/callcongress/state-lookup', method='POST')
gather.say(
"Thank you for calling Call Congress! If you wish to "
+ "call your senators, please enter your 5-digit zip code."
)
response.append(gather)
return Response(str(response), 200, mimetype="application/xml")
Corresponding FlexML Python Syntax
@app.route('/callcongress/welcome', methods=['POST'])
def callcongress():
data = request.get_json()
from_state = data.get('FromState','')
if from_state:
response = f'''
<Response>
<Gather numDigits="1" action="/callcongress/set-state" method="POST">
<Say>Thank you for calling congress! It looks like you\'re calling from {from_state}. If this is correct, please press 1. Press 2 if this is not your current state of residence.</Say>
</Gather>
</Response>'''
else:
response = f'''
<Response>
<Gather numDigits="5" action="/callcongress/state-lookup" method="POST">
<Say>Thank you for calling Call Congress! If you wish to call your senators, please enter your 5-digit zip code.</Say>
</Gather>
</Response>'''
return response
III. state_lookup Route
We modify the state_lookup route like this:
-
Like in the previous callcongress route, we change the way the route receives the data from the call so that JSON data was parsed and the application could get the value from the
Digits
key. -
The
return
statement contains the FlexML code for the Redirect verb.
TwiML Python Code
@app.route('/callcongress/state-lookup', methods=['GET', 'POST'])
def state_lookup():
zip_digits = request.values.get('Digits', None)
zip_obj = Zipcode.query.filter_by(zipcode=zip_digits).first()
return redirect(url_for('call_senators', state_id=zip_obj.state_id))
Corresponding FlexML Python Syntax
@app.route('/callcongress/state-lookup', methods=['GET', 'POST'])
def state_lookup():
data = request.get_json()
zip_digits = data.get('Digits','')
zip_obj = Zipcode.query.filter_by(zipcode=zip_digits).first()
return f'''
<Response>
<Redirect>{url_for('call_senators', state_id=zip_obj.state_id)}</Redirect>
</Response>'''
IV. collect_zip Route
We modify the collect_zip route like this:
-
Remove the use of the Twilio
VoiceResponse()
class. -
Remove the forming of the
Gather
response. -
Remove the forming of the final response which uses the
append()
function. -
Replace the
return
statement with the one containing the FlexML code for theGather
verb.
TwiML Python Code
@app.route('/callcongress/collect-zip', methods=['GET', 'POST'])
def collect_zip():
response = VoiceResponse()
gather = Gather(num_digits=5, action='/callcongress/state-lookup', method='POST')
gather.say(
"If you wish to call your senators, please " + "enter your 5-digit zip code."
)
response.append(gather)
return Response(str(response), 200, mimetype="application/xml")
Corresponding FlexML Python Syntax
@app.route('/callcongress/collect-zip', methods=['GET', 'POST'])
def collect_zip():
return f'''
<Response>
<Gather numDigits="5" action="/callcongress/state-lookup" method="POST">
<Say>If you wish to call your senators, please enter your 5-digit zip code.</Say>
</Gather>
</Response>'''
V. set_state Route
We modify the set_state route like this:
-
Change the way the route receives the data from the call so that JSON data was parsed and the application could get the value from the
Digits
key. -
The same way we change the code for receiving the
CallerState
value. -
The first
return
statement (inside theif
statement) contains the FlexML code for theRedirect
verb. -
And the second
return
statement also contains the FlexML code for anotherRedirect
verb.
TwiML Python Code
@app.route('/callcongress/set-state', methods=['GET', 'POST'])
def set_state():
digits_provided = request.values.get('Digits', None)
if digits_provided == '1':
state = request.values.get('CallerState')
state_obj = State.query.filter_by(name=state).first()
if state_obj:
return redirect(url_for('call_senators', state_id=int(state_obj.id)))
return redirect(url_for('collect_zip'))
Corresponding FlexML Python Syntax
@app.route('/callcongress/set-state', methods=['GET', 'POST'])
def set_state():
data = request.get_json()
digits_provided = data.get('Digits','')
if digits_provided == '1':
state = data.get('CallerState','')
state_obj = State.CallerState.filter_by(name=state).first()
if state_obj:
return f'''
<Response>
<Redirect>{url_for('call_senators', state_id=int(state_obj.id))}</Redirect>
</Response>'''
return f'''
<Response>
<Redirect>{url_for('collect_zip')}</Redirect>
</Response>'''
VI. call_senators Route
We modify the call_senators route like this:
-
Remove the use of the Twilio
VoiceResponse()
class. -
Replace the
return
statement with the one containing the FlexML code for theSay
andDial
verbs.
TwiML Python Code
@app.route('/callcongress/call-senators/<state_id>', methods=['GET', 'POST'])
def call_senators(state_id):
senators = State.query.get(state_id).senators.all()
response = VoiceResponse()
first_call = senators[0]
second_call = senators[1]
response.say(
"Connecting you to {}. ".format(first_call.name)
+ "After the senator's office ends the call, you will "
+ "be re-directed to {}.".format(second_call.name)
)
response.dial(
first_call.phone, action=url_for('call_second_senator', senator_id=second_call.id)
)
return Response(str(response), 200, mimetype="application/xml")
Corresponding FlexML Python Syntax
@app.route('/callcongress/call-senators/<state_id>', methods=['GET', 'POST'])
def call_senators(state_id):
senators = State.query.get(state_id).senators.all()
first_call = senators[0]
second_call = senators[1]
return f'''
<Response>
<Say>Connecting you to {first_call.name}. After the senator's office ends the call, you will be re-directed to {second_call.name}.</Say>
<Dial action="{url_for('call_second_senator', senator_id=second_call.id)}">{first_call.phone}</Dial>
</Response>'''
VII. call_second_senator Route
We modify the call_second_senator route like this:
-
Remove the use of the Twilio
VoiceResponse()
class. -
Remove the forming of the
Say
andDial
responses. -
Replace the
return
statement with the one containing the FlexML code for theSay
andDial
verbs.
TwiML Python Code
@app.route('/callcongress/call-second-senator/<senator_id>', methods=['GET', 'POST'])
def call_second_senator(senator_id):
senator = Senator.query.get(senator_id)
response = VoiceResponse()
response.say("Connecting you to {}.".format(senator.name))
response.dial(senator.phone, action=url_for('end_call'))
return Response(str(response), 200, mimetype="application/xml")
Corresponding FlexML Python Syntax
@app.route('/callcongress/call-second-senator/<senator_id>', methods=['GET', 'POST'])
def call_second_senator(senator_id):
senator = Senator.query.get(senator_id)
return f'''
<Response>
<Say>Connecting you to {senator.name}.</Say>
<Dial action="{url_for('end_call')}">{senator.phone}</Dial>
</Response>'''
VIII. end_call Route
Finally, we modify the end_call route like this:
-
Remove the use of the Twilio
VoiceResponse()
class. -
Remove the forming of the
Say
and Hangup responses. -
Replace the
return
statement with the one containing the FlexML code for theSay
andHangup
verbs.
TwiML Python Code
@app.route('/callcongress/goodbye', methods=['GET', 'POST'])
def end_call():
response = VoiceResponse()
response.say(
"Thank you for using Call Congress! " + "Your voice makes a difference. Goodbye."
)
response.hangup()
return Response(str(response), 200, mimetype="application/xml")
Corresponding FlexML Python Syntax
@app.route('/callcongress/goodbye', methods=['GET', 'POST'])
def end_call():
return f'''
<Response>
<Say>Thank you for using Call Congress! Your voice makes a difference. Goodbye.</Say>
<Hangup/>
</Response>'''
Finishing Migration
Now that we modified all the routes, we can safely remove the Twilio library import declaration from the beginning of the views.py
file:
from twilio.twiml.voice_response import VoiceResponse, Gather
from call_forward_flask import app
from call_forward_flask.models import (
Senator,
State,
Zipcode,
)
from flask import (
Response,
redirect,
render_template,
request,
url_for,
)
@app.route('/')
def hello():
"""Very basic route to landing page."""
return render_template('index.html')
@app.route('/callcongress/welcome', methods=['POST'])
def callcongress():
"""Verify or collect State information."""
data = request.get_json()
from_state = data.get('FromState','')
if from_state:
response = f'''
<Response>
<Gather numDigits="1" action="/callcongress/set-state" method="POST">
<Say>Thank you for calling congress! It looks like you\'re calling from {from_state}. If this is correct, please press 1. Press 2 if this is not your current state of residence.</Say>
</Gather>
</Response>'''
else:
response = f'''
<Response>
<Gather numDigits="5" action="/callcongress/state-lookup" method="POST">
<Say>Thank you for calling Call Congress! If you wish to call your senators, please enter your 5-digit zip code.</Say>
</Gather>
</Response>'''
return response
@app.route('/callcongress/state-lookup', methods=['GET', 'POST'])
def state_lookup():
"""Look up state from given zipcode.
Once state is found, redirect to call_senators for forwarding.
"""
data = request.get_json()
zip_digits = data.get('Digits','')
# NB: We don't do any error handling for a missing/erroneous zip code
# in this sample application. You, gentle reader, should to handle that
# edge case before deploying this code.
zip_obj = Zipcode.query.filter_by(zipcode=zip_digits).first()
return f'''
<Response>
<Redirect>{url_for('call_senators', state_id=zip_obj.state_id)}</Redirect>
</Response>'''
@app.route('/callcongress/collect-zip', methods=['GET', 'POST'])
def collect_zip():
"""If our state guess is wrong, prompt user for zip code."""
return f'''
<Response>
<Gather numDigits="5" action="/callcongress/state-lookup" method="POST">
<Say>If you wish to call your senators, please enter your 5-digit zip code.</Say>
</Gather>
</Response>'''
@app.route('/callcongress/set-state', methods=['GET', 'POST'])
def set_state():
"""Set state for senator call list.
Set user's state from confirmation or user-provided Zip.
Redirect to call_senators route.
"""
# Get the digit pressed by the user
data = request.get_json()
digits_provided = data.get('Digits','')
# Set state if State correct, else prompt for zipcode.
if digits_provided == '1':
state = data.get('CallerState','')
state_obj = State.CallerState.filter_by(name=state).first()
if state_obj:
return f'''
<Response>
<Redirect>{url_for('call_senators', state_id=int(state_obj.id))}</Redirect>
</Response>'''
return f'''
<Response>
<Redirect>{url_for('collect_zip')}</Redirect>
</Response>'''
@app.route('/callcongress/call-senators/<state_id>', methods=['GET', 'POST'])
def call_senators(state_id):
"""Route for connecting caller to both of their senators."""
senators = State.query.get(state_id).senators.all()
first_call = senators[0]
second_call = senators[1]
return f'''
<Response>
<Say>Connecting you to {first_call.name}. After the senator's office ends the call, you will be re-directed to {second_call.name}.</Say>
<Dial action="{url_for('call_second_senator', senator_id=second_call.id)}">{first_call.phone}</Dial>
</Response>'''
@app.route('/callcongress/call-second-senator/<senator_id>', methods=['GET', 'POST'])
def call_second_senator(senator_id):
"""Forward the caller to their second senator."""
senator = Senator.query.get(senator_id)
return f'''
<Response>
<Say>Connecting you to {senator.name}.</Say>
<Dial action="{url_for('end_call')}">{senator.phone}</Dial>
</Response>'''
@app.route('/callcongress/goodbye', methods=['GET', 'POST'])
def end_call():
"""Thank user & hang up."""
return f'''
<Response>
<Say>Thank you for using Call Congress! Your voice makes a difference. Goodbye.</Say>
<Hangup/>
</Response>'''
You can also remove the importing of Twilio modules from the requirements-to-freeze.txt
and requirements.txt
files.
Follow the instructions from the application GitHub page to run the application. Then associate the application with the CarrierX phone number, and call that number to check how the application works.
Further Reading
You have successfully migrated the Call Forwarding application from TwiML to FlexML!
Refer to the following pages to learn more about FlexML verbs and how to use them, and about ways to set up a FlexML endpoint:
Use our Migrating from Twilio to CarrierX Quick Start to learn more about other difficulties you can meet while migrating from Twilio to CarrierX and the ways to solve these issues.
Read other instructions on real-case migrations from Twilio to CarrierX here:
- Send SMS During Calls Application Migration
- Lead Alerts Application Migration
- Server Notifications Application Migration
- Automated Survey Application Migration
Refer to our other quick start guides for instructions on how to work with CarrierX: