Skip to contentSkip to navigationSkip to topbar
On this page

Masked Phone Numbers with Python and Flask


This Flask(link takes you to an external page) sample application is modeled after the rental experience created by AirBnB(link takes you to an external page) but with more Klingons(link takes you to an external page).

Host users can offer rental properties which other guest users can reserve. The guest and the host can then anonymously communicate via a disposable Twilio phone number created just for a reservation. In this tutorial, we'll show you the key bits of code to make this work.

To run this sample app yourself, download the code and follow the instructions on GitHub(link takes you to an external page).

(warning)

Legal implications of managing communications between users

If you choose to manage communications between your users, including voice calls, text-based messages (e.g., SMS), and chat, you may need to comply with certain laws and regulations, including those regarding obtaining consent. Additional information regarding legal compliance considerations and best practices for using Twilio to manage and record communications between your users, such as when using Twilio Proxy, can be found here(link takes you to an external page).

Notice: Twilio recommends that you consult with your legal counsel to make sure that you are complying with all applicable laws in connection with communications you record or store using Twilio.

Read how Lyft uses masked phone numbers to let customers securely contact drivers.(link takes you to an external page)


Create a Reservation

create-a-reservation page anchor

The first step in connecting a guest and a host is creating a reservation.

We handle here a submission form for a new reservation. After we save the reservation to the database, we send the host an SMS message asking them to accept or reject the reservation.

Create A New Reservation

create-a-new-reservation page anchor
1
from twilio.twiml.messaging_response import MessagingResponse
2
from twilio.twiml.voice_response import VoiceResponse
3
4
from airtng_flask import db, bcrypt, app, login_manager
5
from flask import g, request
6
from flask.ext.login import login_user, logout_user, current_user, login_required
7
8
from airtng_flask.forms import RegisterForm, LoginForm, VacationPropertyForm, ReservationForm, \
9
ReservationConfirmationForm, ExchangeForm
10
from airtng_flask.view_helpers import twiml, view, redirect_to, view_with_params
11
from airtng_flask.models import init_models_module
12
13
init_models_module(db, bcrypt, app)
14
15
from airtng_flask.models.user import User
16
from airtng_flask.models.vacation_property import VacationProperty
17
from airtng_flask.models.reservation import Reservation
18
19
20
@app.route('/', methods=["GET", "POST"])
21
@app.route('/register', methods=["GET", "POST"])
22
def register():
23
form = RegisterForm()
24
if request.method == 'POST':
25
if form.validate_on_submit():
26
27
if User.query.filter(User.email == form.email.data).count() > 0:
28
form.email.errors.append("Email address already in use.")
29
return view('register', form)
30
31
user = User(
32
name=form.name.data,
33
email=form.email.data,
34
password=form.password.data,
35
phone_number="+{0}{1}".format(form.country_code.data, form.phone_number.data),
36
area_code=str(form.phone_number.data)[0:3])
37
38
db.session.add(user)
39
db.session.commit()
40
login_user(user, remember=True)
41
42
return redirect_to('home')
43
else:
44
return view('register', form)
45
46
return view('register', form)
47
48
49
@app.route('/login', methods=["GET", "POST"])
50
def login():
51
form = LoginForm()
52
if request.method == 'POST':
53
if form.validate_on_submit():
54
candidate_user = User.query.filter(User.email == form.email.data).first()
55
56
if candidate_user is None or not bcrypt.check_password_hash(candidate_user.password,
57
form.password.data):
58
form.password.errors.append("Invalid credentials.")
59
return view('login', form)
60
61
login_user(candidate_user, remember=True)
62
return redirect_to('home')
63
return view('login', form)
64
65
66
@app.route('/logout', methods=["POST"])
67
@login_required
68
def logout():
69
logout_user()
70
return redirect_to('home')
71
72
73
@app.route('/home', methods=["GET"])
74
@login_required
75
def home():
76
return view('home')
77
78
79
@app.route('/properties', methods=["GET"])
80
@login_required
81
def properties():
82
vacation_properties = VacationProperty.query.all()
83
return view_with_params('properties', vacation_properties=vacation_properties)
84
85
86
@app.route('/properties/new', methods=["GET", "POST"])
87
@login_required
88
def new_property():
89
form = VacationPropertyForm()
90
if request.method == 'POST':
91
if form.validate_on_submit():
92
host = User.query.get(current_user.get_id())
93
94
property = VacationProperty(form.description.data, form.image_url.data, host)
95
db.session.add(property)
96
db.session.commit()
97
return redirect_to('properties')
98
99
return view('property_new', form)
100
101
102
@app.route('/reservations/', methods=["POST"], defaults={'property_id': None})
103
@app.route('/reservations/<property_id>', methods=["GET", "POST"])
104
@login_required
105
def new_reservation(property_id):
106
vacation_property = None
107
form = ReservationForm()
108
form.property_id.data = property_id
109
110
if request.method == 'POST':
111
if form.validate_on_submit():
112
guest = User.query.get(current_user.get_id())
113
114
vacation_property = VacationProperty.query.get(form.property_id.data)
115
reservation = Reservation(form.message.data, vacation_property, guest)
116
db.session.add(reservation)
117
db.session.commit()
118
119
reservation.notify_host()
120
121
return redirect_to('properties')
122
123
if property_id is not None:
124
vacation_property = VacationProperty.query.get(property_id)
125
126
return view_with_params('reservation', vacation_property=vacation_property, form=form)
127
128
129
@app.route('/reservations', methods=["GET"])
130
@login_required
131
def reservations():
132
user = User.query.get(current_user.get_id())
133
134
reservations_as_host = Reservation.query \
135
.filter(VacationProperty.host_id == current_user.get_id() and len(VacationProperty.reservations) > 0) \
136
.join(VacationProperty) \
137
.filter(Reservation.vacation_property_id == VacationProperty.id) \
138
.all()
139
140
reservations_as_guest = user.reservations
141
142
return view_with_params('reservations',
143
reservations_as_guest=reservations_as_guest,
144
reservations_as_host=reservations_as_host)
145
146
147
@app.route('/reservations/confirm', methods=["POST"])
148
def confirm_reservation():
149
form = ReservationConfirmationForm()
150
sms_response_text = "Sorry, it looks like you don't have any reservations to respond to."
151
152
user = User.query.filter(User.phone_number == form.From.data).first()
153
reservation = Reservation \
154
.query \
155
.filter(Reservation.status == 'pending'
156
and Reservation.vacation_property.host.id == user.id) \
157
.first()
158
159
if reservation is not None:
160
161
if 'yes' in form.Body.data or 'accept' in form.Body.data:
162
reservation.confirm()
163
reservation.buy_number(user.area_code)
164
else:
165
reservation.reject()
166
167
db.session.commit()
168
169
sms_response_text = "You have successfully {0} the reservation".format(reservation.status)
170
reservation.notify_guest()
171
172
return twiml(_respond_message(sms_response_text))
173
174
175
@app.route('/exchange/sms', methods=["POST"])
176
def exchange_sms():
177
form = ExchangeForm()
178
179
outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)
180
181
response = MessagingResponse()
182
response.message(form.Body.data, to=outgoing_number)
183
return twiml(response)
184
185
186
@app.route('/exchange/voice', methods=["POST"])
187
def exchange_voice():
188
form = ExchangeForm()
189
outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)
190
191
response = VoiceResponse()
192
response.play("http://howtodocs.s3.amazonaws.com/howdy-tng.mp3")
193
response.dial(outgoing_number)
194
return twiml(response)
195
196
197
# controller utils
198
@app.before_request
199
def before_request():
200
g.user = current_user
201
uri_pattern = request.url_rule
202
if current_user.is_authenticated and (
203
uri_pattern == '/' or uri_pattern == '/login' or uri_pattern == '/register'):
204
redirect_to('home')
205
206
207
@login_manager.user_loader
208
def load_user(id):
209
try:
210
return User.query.get(id)
211
except:
212
return None
213
214
215
def _gather_outgoing_phone_number(incoming_phone_number, anonymous_phone_number):
216
reservation = Reservation.query \
217
.filter(Reservation.anonymous_phone_number == anonymous_phone_number) \
218
.first()
219
220
if reservation is None:
221
raise Exception('Reservation not found for {0}'.format(incoming_phone_number))
222
223
if reservation.guest.phone_number == incoming_phone_number:
224
return reservation.vacation_property.host.phone_number
225
226
return reservation.guest.phone_number
227
228
229
def _respond_message(message):
230
response = MessagingResponse()
231
response.message(message)
232
return response

Part of our reservation system is receiving reservation requests from potential renters. However, these reservations need to be confirmed. Let's see how we would handle this step.


Before the reservation is finalized, the host needs to confirm that the property was reserved. Learn how to automate this process on our first AirTNG tutorial Workflow Automation.

1
from twilio.twiml.messaging_response import MessagingResponse
2
from twilio.twiml.voice_response import VoiceResponse
3
4
from airtng_flask import db, bcrypt, app, login_manager
5
from flask import g, request
6
from flask.ext.login import login_user, logout_user, current_user, login_required
7
8
from airtng_flask.forms import RegisterForm, LoginForm, VacationPropertyForm, ReservationForm, \
9
ReservationConfirmationForm, ExchangeForm
10
from airtng_flask.view_helpers import twiml, view, redirect_to, view_with_params
11
from airtng_flask.models import init_models_module
12
13
init_models_module(db, bcrypt, app)
14
15
from airtng_flask.models.user import User
16
from airtng_flask.models.vacation_property import VacationProperty
17
from airtng_flask.models.reservation import Reservation
18
19
20
@app.route('/', methods=["GET", "POST"])
21
@app.route('/register', methods=["GET", "POST"])
22
def register():
23
form = RegisterForm()
24
if request.method == 'POST':
25
if form.validate_on_submit():
26
27
if User.query.filter(User.email == form.email.data).count() > 0:
28
form.email.errors.append("Email address already in use.")
29
return view('register', form)
30
31
user = User(
32
name=form.name.data,
33
email=form.email.data,
34
password=form.password.data,
35
phone_number="+{0}{1}".format(form.country_code.data, form.phone_number.data),
36
area_code=str(form.phone_number.data)[0:3])
37
38
db.session.add(user)
39
db.session.commit()
40
login_user(user, remember=True)
41
42
return redirect_to('home')
43
else:
44
return view('register', form)
45
46
return view('register', form)
47
48
49
@app.route('/login', methods=["GET", "POST"])
50
def login():
51
form = LoginForm()
52
if request.method == 'POST':
53
if form.validate_on_submit():
54
candidate_user = User.query.filter(User.email == form.email.data).first()
55
56
if candidate_user is None or not bcrypt.check_password_hash(candidate_user.password,
57
form.password.data):
58
form.password.errors.append("Invalid credentials.")
59
return view('login', form)
60
61
login_user(candidate_user, remember=True)
62
return redirect_to('home')
63
return view('login', form)
64
65
66
@app.route('/logout', methods=["POST"])
67
@login_required
68
def logout():
69
logout_user()
70
return redirect_to('home')
71
72
73
@app.route('/home', methods=["GET"])
74
@login_required
75
def home():
76
return view('home')
77
78
79
@app.route('/properties', methods=["GET"])
80
@login_required
81
def properties():
82
vacation_properties = VacationProperty.query.all()
83
return view_with_params('properties', vacation_properties=vacation_properties)
84
85
86
@app.route('/properties/new', methods=["GET", "POST"])
87
@login_required
88
def new_property():
89
form = VacationPropertyForm()
90
if request.method == 'POST':
91
if form.validate_on_submit():
92
host = User.query.get(current_user.get_id())
93
94
property = VacationProperty(form.description.data, form.image_url.data, host)
95
db.session.add(property)
96
db.session.commit()
97
return redirect_to('properties')
98
99
return view('property_new', form)
100
101
102
@app.route('/reservations/', methods=["POST"], defaults={'property_id': None})
103
@app.route('/reservations/<property_id>', methods=["GET", "POST"])
104
@login_required
105
def new_reservation(property_id):
106
vacation_property = None
107
form = ReservationForm()
108
form.property_id.data = property_id
109
110
if request.method == 'POST':
111
if form.validate_on_submit():
112
guest = User.query.get(current_user.get_id())
113
114
vacation_property = VacationProperty.query.get(form.property_id.data)
115
reservation = Reservation(form.message.data, vacation_property, guest)
116
db.session.add(reservation)
117
db.session.commit()
118
119
reservation.notify_host()
120
121
return redirect_to('properties')
122
123
if property_id is not None:
124
vacation_property = VacationProperty.query.get(property_id)
125
126
return view_with_params('reservation', vacation_property=vacation_property, form=form)
127
128
129
@app.route('/reservations', methods=["GET"])
130
@login_required
131
def reservations():
132
user = User.query.get(current_user.get_id())
133
134
reservations_as_host = Reservation.query \
135
.filter(VacationProperty.host_id == current_user.get_id() and len(VacationProperty.reservations) > 0) \
136
.join(VacationProperty) \
137
.filter(Reservation.vacation_property_id == VacationProperty.id) \
138
.all()
139
140
reservations_as_guest = user.reservations
141
142
return view_with_params('reservations',
143
reservations_as_guest=reservations_as_guest,
144
reservations_as_host=reservations_as_host)
145
146
147
@app.route('/reservations/confirm', methods=["POST"])
148
def confirm_reservation():
149
form = ReservationConfirmationForm()
150
sms_response_text = "Sorry, it looks like you don't have any reservations to respond to."
151
152
user = User.query.filter(User.phone_number == form.From.data).first()
153
reservation = Reservation \
154
.query \
155
.filter(Reservation.status == 'pending'
156
and Reservation.vacation_property.host.id == user.id) \
157
.first()
158
159
if reservation is not None:
160
161
if 'yes' in form.Body.data or 'accept' in form.Body.data:
162
reservation.confirm()
163
reservation.buy_number(user.area_code)
164
else:
165
reservation.reject()
166
167
db.session.commit()
168
169
sms_response_text = "You have successfully {0} the reservation".format(reservation.status)
170
reservation.notify_guest()
171
172
return twiml(_respond_message(sms_response_text))
173
174
175
@app.route('/exchange/sms', methods=["POST"])
176
def exchange_sms():
177
form = ExchangeForm()
178
179
outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)
180
181
response = MessagingResponse()
182
response.message(form.Body.data, to=outgoing_number)
183
return twiml(response)
184
185
186
@app.route('/exchange/voice', methods=["POST"])
187
def exchange_voice():
188
form = ExchangeForm()
189
outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)
190
191
response = VoiceResponse()
192
response.play("http://howtodocs.s3.amazonaws.com/howdy-tng.mp3")
193
response.dial(outgoing_number)
194
return twiml(response)
195
196
197
# controller utils
198
@app.before_request
199
def before_request():
200
g.user = current_user
201
uri_pattern = request.url_rule
202
if current_user.is_authenticated and (
203
uri_pattern == '/' or uri_pattern == '/login' or uri_pattern == '/register'):
204
redirect_to('home')
205
206
207
@login_manager.user_loader
208
def load_user(id):
209
try:
210
return User.query.get(id)
211
except:
212
return None
213
214
215
def _gather_outgoing_phone_number(incoming_phone_number, anonymous_phone_number):
216
reservation = Reservation.query \
217
.filter(Reservation.anonymous_phone_number == anonymous_phone_number) \
218
.first()
219
220
if reservation is None:
221
raise Exception('Reservation not found for {0}'.format(incoming_phone_number))
222
223
if reservation.guest.phone_number == incoming_phone_number:
224
return reservation.vacation_property.host.phone_number
225
226
return reservation.guest.phone_number
227
228
229
def _respond_message(message):
230
response = MessagingResponse()
231
response.message(message)
232
return response

Once the reservation is confirmed, we need to purchase a Twilio number that the guest and host can use to communicate.


Purchase a Twilio Number

purchase-a-twilio-number page anchor

Here we use the Twilio Python helper library to search for and buy a new phone number to associate with the reservation. We start by searching for a number with a local area code - if we can't find one, we take any available phone number in that country.

When we buy the number, we designate a TwiML Application that will handle webhook(link takes you to an external page) requests when the new number receives an incoming call or text.

We then save the new phone number on our Reservation model. Therefore when our app receives calls or messages to this number we know which reservation the call or text belongs to.

airtng_flask/models/reservation.py

1
from airtng_flask.models import app_db, auth_token, account_sid, phone_number, application_sid
2
from flask import render_template
3
from twilio.rest import Client
4
5
DB = app_db()
6
7
8
class Reservation(DB.Model):
9
__tablename__ = "reservations"
10
11
id = DB.Column(DB.Integer, primary_key=True)
12
message = DB.Column(DB.String, nullable=False)
13
status = DB.Column(DB.Enum('pending', 'confirmed', 'rejected', name='reservation_status_enum'),
14
default='pending')
15
anonymous_phone_number = DB.Column(DB.String, nullable=True)
16
guest_id = DB.Column(DB.Integer, DB.ForeignKey('users.id'))
17
vacation_property_id = DB.Column(DB.Integer, DB.ForeignKey('vacation_properties.id'))
18
guest = DB.relationship("User", back_populates="reservations")
19
vacation_property = DB.relationship("VacationProperty", back_populates="reservations")
20
21
def __init__(self, message, vacation_property, guest):
22
self.message = message
23
self.guest = guest
24
self.vacation_property = vacation_property
25
self.status = 'pending'
26
27
def confirm(self):
28
self.status = 'confirmed'
29
30
def reject(self):
31
self.status = 'rejected'
32
33
def __repr__(self):
34
return '<Reservation {0}>'.format(self.id)
35
36
def notify_host(self):
37
self._send_message(self.vacation_property.host.phone_number,
38
render_template('messages/sms_host.txt',
39
name=self.guest.name,
40
description=self.vacation_property.description,
41
message=self.message))
42
43
def notify_guest(self):
44
self._send_message(self.guest.phone_number,
45
render_template('messages/sms_guest.txt',
46
description=self.vacation_property.description,
47
status=self.status))
48
49
def buy_number(self, area_code):
50
numbers = self._get_twilio_client().available_phone_numbers("US") \
51
.local \
52
.list(area_code=area_code,
53
sms_enabled=True,
54
voice_enabled=True)
55
56
if numbers:
57
number = self._purchase_number(numbers[0])
58
self.anonymous_phone_number = number
59
return number
60
else:
61
numbers = self._get_twilio_client().available_phone_numbers("US") \
62
.local \
63
.list(sms_enabled=True, voice_enabled=True)
64
65
if numbers:
66
number = self._purchase_number(numbers[0])
67
self.anonymous_phone_number = number
68
return number
69
70
return None
71
72
def _purchase_number(self, number):
73
return self._get_twilio_client().incoming_phone_numbers \
74
.create(sms_application_sid=application_sid(),
75
voice_application_sid=application_sid(),
76
phone_number=number) \
77
.phone_number
78
79
def _get_twilio_client(self):
80
return Client(account_sid(), auth_token())
81
82
def _send_message(self, to, message):
83
self._get_twilio_client().messages \
84
.create(to,
85
from_=phone_number(),
86
body=message)

Now that each reservation has a Twilio Phone Number, we can see how the application will look up reservations as guest or host calls come in.


When someone messages or calls one of the Twilio numbers (that we purchased for a reservation) Twilio makes a request to the URL you set in the TwiML app. That request will contain some helpful metadata:

  • The incoming_phone_number number that originally called or sent an SMS.
  • The anonymous_phone_number Twilio number that triggered this request.

Take a look at Twilio's SMS Documentation and Twilio's Voice Documentation for a full list of the parameters you can use.

In our code we use the To parameter sent by Twilio to find a reservation that has the number we bought stored in it, as this is the number both hosts and guests will call and send SMSs to.

1
from twilio.twiml.messaging_response import MessagingResponse
2
from twilio.twiml.voice_response import VoiceResponse
3
4
from airtng_flask import db, bcrypt, app, login_manager
5
from flask import g, request
6
from flask.ext.login import login_user, logout_user, current_user, login_required
7
8
from airtng_flask.forms import RegisterForm, LoginForm, VacationPropertyForm, ReservationForm, \
9
ReservationConfirmationForm, ExchangeForm
10
from airtng_flask.view_helpers import twiml, view, redirect_to, view_with_params
11
from airtng_flask.models import init_models_module
12
13
init_models_module(db, bcrypt, app)
14
15
from airtng_flask.models.user import User
16
from airtng_flask.models.vacation_property import VacationProperty
17
from airtng_flask.models.reservation import Reservation
18
19
20
@app.route('/', methods=["GET", "POST"])
21
@app.route('/register', methods=["GET", "POST"])
22
def register():
23
form = RegisterForm()
24
if request.method == 'POST':
25
if form.validate_on_submit():
26
27
if User.query.filter(User.email == form.email.data).count() > 0:
28
form.email.errors.append("Email address already in use.")
29
return view('register', form)
30
31
user = User(
32
name=form.name.data,
33
email=form.email.data,
34
password=form.password.data,
35
phone_number="+{0}{1}".format(form.country_code.data, form.phone_number.data),
36
area_code=str(form.phone_number.data)[0:3])
37
38
db.session.add(user)
39
db.session.commit()
40
login_user(user, remember=True)
41
42
return redirect_to('home')
43
else:
44
return view('register', form)
45
46
return view('register', form)
47
48
49
@app.route('/login', methods=["GET", "POST"])
50
def login():
51
form = LoginForm()
52
if request.method == 'POST':
53
if form.validate_on_submit():
54
candidate_user = User.query.filter(User.email == form.email.data).first()
55
56
if candidate_user is None or not bcrypt.check_password_hash(candidate_user.password,
57
form.password.data):
58
form.password.errors.append("Invalid credentials.")
59
return view('login', form)
60
61
login_user(candidate_user, remember=True)
62
return redirect_to('home')
63
return view('login', form)
64
65
66
@app.route('/logout', methods=["POST"])
67
@login_required
68
def logout():
69
logout_user()
70
return redirect_to('home')
71
72
73
@app.route('/home', methods=["GET"])
74
@login_required
75
def home():
76
return view('home')
77
78
79
@app.route('/properties', methods=["GET"])
80
@login_required
81
def properties():
82
vacation_properties = VacationProperty.query.all()
83
return view_with_params('properties', vacation_properties=vacation_properties)
84
85
86
@app.route('/properties/new', methods=["GET", "POST"])
87
@login_required
88
def new_property():
89
form = VacationPropertyForm()
90
if request.method == 'POST':
91
if form.validate_on_submit():
92
host = User.query.get(current_user.get_id())
93
94
property = VacationProperty(form.description.data, form.image_url.data, host)
95
db.session.add(property)
96
db.session.commit()
97
return redirect_to('properties')
98
99
return view('property_new', form)
100
101
102
@app.route('/reservations/', methods=["POST"], defaults={'property_id': None})
103
@app.route('/reservations/<property_id>', methods=["GET", "POST"])
104
@login_required
105
def new_reservation(property_id):
106
vacation_property = None
107
form = ReservationForm()
108
form.property_id.data = property_id
109
110
if request.method == 'POST':
111
if form.validate_on_submit():
112
guest = User.query.get(current_user.get_id())
113
114
vacation_property = VacationProperty.query.get(form.property_id.data)
115
reservation = Reservation(form.message.data, vacation_property, guest)
116
db.session.add(reservation)
117
db.session.commit()
118
119
reservation.notify_host()
120
121
return redirect_to('properties')
122
123
if property_id is not None:
124
vacation_property = VacationProperty.query.get(property_id)
125
126
return view_with_params('reservation', vacation_property=vacation_property, form=form)
127
128
129
@app.route('/reservations', methods=["GET"])
130
@login_required
131
def reservations():
132
user = User.query.get(current_user.get_id())
133
134
reservations_as_host = Reservation.query \
135
.filter(VacationProperty.host_id == current_user.get_id() and len(VacationProperty.reservations) > 0) \
136
.join(VacationProperty) \
137
.filter(Reservation.vacation_property_id == VacationProperty.id) \
138
.all()
139
140
reservations_as_guest = user.reservations
141
142
return view_with_params('reservations',
143
reservations_as_guest=reservations_as_guest,
144
reservations_as_host=reservations_as_host)
145
146
147
@app.route('/reservations/confirm', methods=["POST"])
148
def confirm_reservation():
149
form = ReservationConfirmationForm()
150
sms_response_text = "Sorry, it looks like you don't have any reservations to respond to."
151
152
user = User.query.filter(User.phone_number == form.From.data).first()
153
reservation = Reservation \
154
.query \
155
.filter(Reservation.status == 'pending'
156
and Reservation.vacation_property.host.id == user.id) \
157
.first()
158
159
if reservation is not None:
160
161
if 'yes' in form.Body.data or 'accept' in form.Body.data:
162
reservation.confirm()
163
reservation.buy_number(user.area_code)
164
else:
165
reservation.reject()
166
167
db.session.commit()
168
169
sms_response_text = "You have successfully {0} the reservation".format(reservation.status)
170
reservation.notify_guest()
171
172
return twiml(_respond_message(sms_response_text))
173
174
175
@app.route('/exchange/sms', methods=["POST"])
176
def exchange_sms():
177
form = ExchangeForm()
178
179
outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)
180
181
response = MessagingResponse()
182
response.message(form.Body.data, to=outgoing_number)
183
return twiml(response)
184
185
186
@app.route('/exchange/voice', methods=["POST"])
187
def exchange_voice():
188
form = ExchangeForm()
189
outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)
190
191
response = VoiceResponse()
192
response.play("http://howtodocs.s3.amazonaws.com/howdy-tng.mp3")
193
response.dial(outgoing_number)
194
return twiml(response)
195
196
197
# controller utils
198
@app.before_request
199
def before_request():
200
g.user = current_user
201
uri_pattern = request.url_rule
202
if current_user.is_authenticated and (
203
uri_pattern == '/' or uri_pattern == '/login' or uri_pattern == '/register'):
204
redirect_to('home')
205
206
207
@login_manager.user_loader
208
def load_user(id):
209
try:
210
return User.query.get(id)
211
except:
212
return None
213
214
215
def _gather_outgoing_phone_number(incoming_phone_number, anonymous_phone_number):
216
reservation = Reservation.query \
217
.filter(Reservation.anonymous_phone_number == anonymous_phone_number) \
218
.first()
219
220
if reservation is None:
221
raise Exception('Reservation not found for {0}'.format(incoming_phone_number))
222
223
if reservation.guest.phone_number == incoming_phone_number:
224
return reservation.vacation_property.host.phone_number
225
226
return reservation.guest.phone_number
227
228
229
def _respond_message(message):
230
response = MessagingResponse()
231
response.message(message)
232
return response

Next, let's see how to connect the guest and the host via SMS.


Our TwiML application should be configured to send HTTP requests to this controller method on any incoming text message. Our app responds with TwiML to tell Twilio what to do in response to the message.

If the initial message sent to the anonymous number was sent by the host, we forward it on to the guest. Likewise, if the original message was sent by the guest, we forward it to the host.

We wrote a helper function called gather_outgoing_phone_number to help us determine which party to forward the message to.

1
from twilio.twiml.messaging_response import MessagingResponse
2
from twilio.twiml.voice_response import VoiceResponse
3
4
from airtng_flask import db, bcrypt, app, login_manager
5
from flask import g, request
6
from flask.ext.login import login_user, logout_user, current_user, login_required
7
8
from airtng_flask.forms import RegisterForm, LoginForm, VacationPropertyForm, ReservationForm, \
9
ReservationConfirmationForm, ExchangeForm
10
from airtng_flask.view_helpers import twiml, view, redirect_to, view_with_params
11
from airtng_flask.models import init_models_module
12
13
init_models_module(db, bcrypt, app)
14
15
from airtng_flask.models.user import User
16
from airtng_flask.models.vacation_property import VacationProperty
17
from airtng_flask.models.reservation import Reservation
18
19
20
@app.route('/', methods=["GET", "POST"])
21
@app.route('/register', methods=["GET", "POST"])
22
def register():
23
form = RegisterForm()
24
if request.method == 'POST':
25
if form.validate_on_submit():
26
27
if User.query.filter(User.email == form.email.data).count() > 0:
28
form.email.errors.append("Email address already in use.")
29
return view('register', form)
30
31
user = User(
32
name=form.name.data,
33
email=form.email.data,
34
password=form.password.data,
35
phone_number="+{0}{1}".format(form.country_code.data, form.phone_number.data),
36
area_code=str(form.phone_number.data)[0:3])
37
38
db.session.add(user)
39
db.session.commit()
40
login_user(user, remember=True)
41
42
return redirect_to('home')
43
else:
44
return view('register', form)
45
46
return view('register', form)
47
48
49
@app.route('/login', methods=["GET", "POST"])
50
def login():
51
form = LoginForm()
52
if request.method == 'POST':
53
if form.validate_on_submit():
54
candidate_user = User.query.filter(User.email == form.email.data).first()
55
56
if candidate_user is None or not bcrypt.check_password_hash(candidate_user.password,
57
form.password.data):
58
form.password.errors.append("Invalid credentials.")
59
return view('login', form)
60
61
login_user(candidate_user, remember=True)
62
return redirect_to('home')
63
return view('login', form)
64
65
66
@app.route('/logout', methods=["POST"])
67
@login_required
68
def logout():
69
logout_user()
70
return redirect_to('home')
71
72
73
@app.route('/home', methods=["GET"])
74
@login_required
75
def home():
76
return view('home')
77
78
79
@app.route('/properties', methods=["GET"])
80
@login_required
81
def properties():
82
vacation_properties = VacationProperty.query.all()
83
return view_with_params('properties', vacation_properties=vacation_properties)
84
85
86
@app.route('/properties/new', methods=["GET", "POST"])
87
@login_required
88
def new_property():
89
form = VacationPropertyForm()
90
if request.method == 'POST':
91
if form.validate_on_submit():
92
host = User.query.get(current_user.get_id())
93
94
property = VacationProperty(form.description.data, form.image_url.data, host)
95
db.session.add(property)
96
db.session.commit()
97
return redirect_to('properties')
98
99
return view('property_new', form)
100
101
102
@app.route('/reservations/', methods=["POST"], defaults={'property_id': None})
103
@app.route('/reservations/<property_id>', methods=["GET", "POST"])
104
@login_required
105
def new_reservation(property_id):
106
vacation_property = None
107
form = ReservationForm()
108
form.property_id.data = property_id
109
110
if request.method == 'POST':
111
if form.validate_on_submit():
112
guest = User.query.get(current_user.get_id())
113
114
vacation_property = VacationProperty.query.get(form.property_id.data)
115
reservation = Reservation(form.message.data, vacation_property, guest)
116
db.session.add(reservation)
117
db.session.commit()
118
119
reservation.notify_host()
120
121
return redirect_to('properties')
122
123
if property_id is not None:
124
vacation_property = VacationProperty.query.get(property_id)
125
126
return view_with_params('reservation', vacation_property=vacation_property, form=form)
127
128
129
@app.route('/reservations', methods=["GET"])
130
@login_required
131
def reservations():
132
user = User.query.get(current_user.get_id())
133
134
reservations_as_host = Reservation.query \
135
.filter(VacationProperty.host_id == current_user.get_id() and len(VacationProperty.reservations) > 0) \
136
.join(VacationProperty) \
137
.filter(Reservation.vacation_property_id == VacationProperty.id) \
138
.all()
139
140
reservations_as_guest = user.reservations
141
142
return view_with_params('reservations',
143
reservations_as_guest=reservations_as_guest,
144
reservations_as_host=reservations_as_host)
145
146
147
@app.route('/reservations/confirm', methods=["POST"])
148
def confirm_reservation():
149
form = ReservationConfirmationForm()
150
sms_response_text = "Sorry, it looks like you don't have any reservations to respond to."
151
152
user = User.query.filter(User.phone_number == form.From.data).first()
153
reservation = Reservation \
154
.query \
155
.filter(Reservation.status == 'pending'
156
and Reservation.vacation_property.host.id == user.id) \
157
.first()
158
159
if reservation is not None:
160
161
if 'yes' in form.Body.data or 'accept' in form.Body.data:
162
reservation.confirm()
163
reservation.buy_number(user.area_code)
164
else:
165
reservation.reject()
166
167
db.session.commit()
168
169
sms_response_text = "You have successfully {0} the reservation".format(reservation.status)
170
reservation.notify_guest()
171
172
return twiml(_respond_message(sms_response_text))
173
174
175
@app.route('/exchange/sms', methods=["POST"])
176
def exchange_sms():
177
form = ExchangeForm()
178
179
outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)
180
181
response = MessagingResponse()
182
response.message(form.Body.data, to=outgoing_number)
183
return twiml(response)
184
185
186
@app.route('/exchange/voice', methods=["POST"])
187
def exchange_voice():
188
form = ExchangeForm()
189
outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)
190
191
response = VoiceResponse()
192
response.play("http://howtodocs.s3.amazonaws.com/howdy-tng.mp3")
193
response.dial(outgoing_number)
194
return twiml(response)
195
196
197
# controller utils
198
@app.before_request
199
def before_request():
200
g.user = current_user
201
uri_pattern = request.url_rule
202
if current_user.is_authenticated and (
203
uri_pattern == '/' or uri_pattern == '/login' or uri_pattern == '/register'):
204
redirect_to('home')
205
206
207
@login_manager.user_loader
208
def load_user(id):
209
try:
210
return User.query.get(id)
211
except:
212
return None
213
214
215
def _gather_outgoing_phone_number(incoming_phone_number, anonymous_phone_number):
216
reservation = Reservation.query \
217
.filter(Reservation.anonymous_phone_number == anonymous_phone_number) \
218
.first()
219
220
if reservation is None:
221
raise Exception('Reservation not found for {0}'.format(incoming_phone_number))
222
223
if reservation.guest.phone_number == incoming_phone_number:
224
return reservation.vacation_property.host.phone_number
225
226
return reservation.guest.phone_number
227
228
229
def _respond_message(message):
230
response = MessagingResponse()
231
response.message(message)
232
return response

Let's see how to connect the guest and the host via phone call next.


Our Twilio application will send HTTP requests to this method on any incoming voice call. Our app responds with TwiML instructions that tell Twilio to Play an introductory MP3 audio file and then Dial either the guest or host, depending on who initiated the call.

1
from twilio.twiml.messaging_response import MessagingResponse
2
from twilio.twiml.voice_response import VoiceResponse
3
4
from airtng_flask import db, bcrypt, app, login_manager
5
from flask import g, request
6
from flask.ext.login import login_user, logout_user, current_user, login_required
7
8
from airtng_flask.forms import RegisterForm, LoginForm, VacationPropertyForm, ReservationForm, \
9
ReservationConfirmationForm, ExchangeForm
10
from airtng_flask.view_helpers import twiml, view, redirect_to, view_with_params
11
from airtng_flask.models import init_models_module
12
13
init_models_module(db, bcrypt, app)
14
15
from airtng_flask.models.user import User
16
from airtng_flask.models.vacation_property import VacationProperty
17
from airtng_flask.models.reservation import Reservation
18
19
20
@app.route('/', methods=["GET", "POST"])
21
@app.route('/register', methods=["GET", "POST"])
22
def register():
23
form = RegisterForm()
24
if request.method == 'POST':
25
if form.validate_on_submit():
26
27
if User.query.filter(User.email == form.email.data).count() > 0:
28
form.email.errors.append("Email address already in use.")
29
return view('register', form)
30
31
user = User(
32
name=form.name.data,
33
email=form.email.data,
34
password=form.password.data,
35
phone_number="+{0}{1}".format(form.country_code.data, form.phone_number.data),
36
area_code=str(form.phone_number.data)[0:3])
37
38
db.session.add(user)
39
db.session.commit()
40
login_user(user, remember=True)
41
42
return redirect_to('home')
43
else:
44
return view('register', form)
45
46
return view('register', form)
47
48
49
@app.route('/login', methods=["GET", "POST"])
50
def login():
51
form = LoginForm()
52
if request.method == 'POST':
53
if form.validate_on_submit():
54
candidate_user = User.query.filter(User.email == form.email.data).first()
55
56
if candidate_user is None or not bcrypt.check_password_hash(candidate_user.password,
57
form.password.data):
58
form.password.errors.append("Invalid credentials.")
59
return view('login', form)
60
61
login_user(candidate_user, remember=True)
62
return redirect_to('home')
63
return view('login', form)
64
65
66
@app.route('/logout', methods=["POST"])
67
@login_required
68
def logout():
69
logout_user()
70
return redirect_to('home')
71
72
73
@app.route('/home', methods=["GET"])
74
@login_required
75
def home():
76
return view('home')
77
78
79
@app.route('/properties', methods=["GET"])
80
@login_required
81
def properties():
82
vacation_properties = VacationProperty.query.all()
83
return view_with_params('properties', vacation_properties=vacation_properties)
84
85
86
@app.route('/properties/new', methods=["GET", "POST"])
87
@login_required
88
def new_property():
89
form = VacationPropertyForm()
90
if request.method == 'POST':
91
if form.validate_on_submit():
92
host = User.query.get(current_user.get_id())
93
94
property = VacationProperty(form.description.data, form.image_url.data, host)
95
db.session.add(property)
96
db.session.commit()
97
return redirect_to('properties')
98
99
return view('property_new', form)
100
101
102
@app.route('/reservations/', methods=["POST"], defaults={'property_id': None})
103
@app.route('/reservations/<property_id>', methods=["GET", "POST"])
104
@login_required
105
def new_reservation(property_id):
106
vacation_property = None
107
form = ReservationForm()
108
form.property_id.data = property_id
109
110
if request.method == 'POST':
111
if form.validate_on_submit():
112
guest = User.query.get(current_user.get_id())
113
114
vacation_property = VacationProperty.query.get(form.property_id.data)
115
reservation = Reservation(form.message.data, vacation_property, guest)
116
db.session.add(reservation)
117
db.session.commit()
118
119
reservation.notify_host()
120
121
return redirect_to('properties')
122
123
if property_id is not None:
124
vacation_property = VacationProperty.query.get(property_id)
125
126
return view_with_params('reservation', vacation_property=vacation_property, form=form)
127
128
129
@app.route('/reservations', methods=["GET"])
130
@login_required
131
def reservations():
132
user = User.query.get(current_user.get_id())
133
134
reservations_as_host = Reservation.query \
135
.filter(VacationProperty.host_id == current_user.get_id() and len(VacationProperty.reservations) > 0) \
136
.join(VacationProperty) \
137
.filter(Reservation.vacation_property_id == VacationProperty.id) \
138
.all()
139
140
reservations_as_guest = user.reservations
141
142
return view_with_params('reservations',
143
reservations_as_guest=reservations_as_guest,
144
reservations_as_host=reservations_as_host)
145
146
147
@app.route('/reservations/confirm', methods=["POST"])
148
def confirm_reservation():
149
form = ReservationConfirmationForm()
150
sms_response_text = "Sorry, it looks like you don't have any reservations to respond to."
151
152
user = User.query.filter(User.phone_number == form.From.data).first()
153
reservation = Reservation \
154
.query \
155
.filter(Reservation.status == 'pending'
156
and Reservation.vacation_property.host.id == user.id) \
157
.first()
158
159
if reservation is not None:
160
161
if 'yes' in form.Body.data or 'accept' in form.Body.data:
162
reservation.confirm()
163
reservation.buy_number(user.area_code)
164
else:
165
reservation.reject()
166
167
db.session.commit()
168
169
sms_response_text = "You have successfully {0} the reservation".format(reservation.status)
170
reservation.notify_guest()
171
172
return twiml(_respond_message(sms_response_text))
173
174
175
@app.route('/exchange/sms', methods=["POST"])
176
def exchange_sms():
177
form = ExchangeForm()
178
179
outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)
180
181
response = MessagingResponse()
182
response.message(form.Body.data, to=outgoing_number)
183
return twiml(response)
184
185
186
@app.route('/exchange/voice', methods=["POST"])
187
def exchange_voice():
188
form = ExchangeForm()
189
outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)
190
191
response = VoiceResponse()
192
response.play("http://howtodocs.s3.amazonaws.com/howdy-tng.mp3")
193
response.dial(outgoing_number)
194
return twiml(response)
195
196
197
# controller utils
198
@app.before_request
199
def before_request():
200
g.user = current_user
201
uri_pattern = request.url_rule
202
if current_user.is_authenticated and (
203
uri_pattern == '/' or uri_pattern == '/login' or uri_pattern == '/register'):
204
redirect_to('home')
205
206
207
@login_manager.user_loader
208
def load_user(id):
209
try:
210
return User.query.get(id)
211
except:
212
return None
213
214
215
def _gather_outgoing_phone_number(incoming_phone_number, anonymous_phone_number):
216
reservation = Reservation.query \
217
.filter(Reservation.anonymous_phone_number == anonymous_phone_number) \
218
.first()
219
220
if reservation is None:
221
raise Exception('Reservation not found for {0}'.format(incoming_phone_number))
222
223
if reservation.guest.phone_number == incoming_phone_number:
224
return reservation.vacation_property.host.phone_number
225
226
return reservation.guest.phone_number
227
228
229
def _respond_message(message):
230
response = MessagingResponse()
231
response.message(message)
232
return response

That's it! We've just implemented anonymous communications that allow your customers to connect while protecting their privacy.


If you're a Python developer working with Twilio you might want to check out these other tutorials:

IVR: Phone Tree

Create a seamless customer service experience by building an IVR Phone Tree for your company.

Call Tracking

Measure the effectiveness of different marketing campaigns by assigning a unique phone number to different advertisements and track which ones have the best call rates while getting some data about the callers themselves.

Did this help?

did-this-help page anchor

Thanks for checking out this tutorial! If you have any feedback to share with us, we'd love to hear it. Tweet @twilio(link takes you to an external page) to let us know what you think.

Need some help?

Terms of service

Copyright © 2024 Twilio Inc.