This Flask sample application is modeled after the rental experience created by AirBnB but with more Klingons.
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.
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.
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.
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.
_232from twilio.twiml.messaging_response import MessagingResponse_232from twilio.twiml.voice_response import VoiceResponse_232_232from airtng_flask import db, bcrypt, app, login_manager_232from flask import g, request_232from flask.ext.login import login_user, logout_user, current_user, login_required_232_232from airtng_flask.forms import RegisterForm, LoginForm, VacationPropertyForm, ReservationForm, \_232 ReservationConfirmationForm, ExchangeForm_232from airtng_flask.view_helpers import twiml, view, redirect_to, view_with_params_232from airtng_flask.models import init_models_module_232_232init_models_module(db, bcrypt, app)_232_232from airtng_flask.models.user import User_232from airtng_flask.models.vacation_property import VacationProperty_232from airtng_flask.models.reservation import Reservation_232_232_232@app.route('/', methods=["GET", "POST"])_232@app.route('/register', methods=["GET", "POST"])_232def register():_232 form = RegisterForm()_232 if request.method == 'POST':_232 if form.validate_on_submit():_232_232 if User.query.filter(User.email == form.email.data).count() > 0:_232 form.email.errors.append("Email address already in use.")_232 return view('register', form)_232_232 user = User(_232 name=form.name.data,_232 email=form.email.data,_232 password=form.password.data,_232 phone_number="+{0}{1}".format(form.country_code.data, form.phone_number.data),_232 area_code=str(form.phone_number.data)[0:3])_232_232 db.session.add(user)_232 db.session.commit()_232 login_user(user, remember=True)_232_232 return redirect_to('home')_232 else:_232 return view('register', form)_232_232 return view('register', form)_232_232_232@app.route('/login', methods=["GET", "POST"])_232def login():_232 form = LoginForm()_232 if request.method == 'POST':_232 if form.validate_on_submit():_232 candidate_user = User.query.filter(User.email == form.email.data).first()_232_232 if candidate_user is None or not bcrypt.check_password_hash(candidate_user.password,_232 form.password.data):_232 form.password.errors.append("Invalid credentials.")_232 return view('login', form)_232_232 login_user(candidate_user, remember=True)_232 return redirect_to('home')_232 return view('login', form)_232_232_232@app.route('/logout', methods=["POST"])_232@login_required_232def logout():_232 logout_user()_232 return redirect_to('home')_232_232_232@app.route('/home', methods=["GET"])_232@login_required_232def home():_232 return view('home')_232_232_232@app.route('/properties', methods=["GET"])_232@login_required_232def properties():_232 vacation_properties = VacationProperty.query.all()_232 return view_with_params('properties', vacation_properties=vacation_properties)_232_232_232@app.route('/properties/new', methods=["GET", "POST"])_232@login_required_232def new_property():_232 form = VacationPropertyForm()_232 if request.method == 'POST':_232 if form.validate_on_submit():_232 host = User.query.get(current_user.get_id())_232_232 property = VacationProperty(form.description.data, form.image_url.data, host)_232 db.session.add(property)_232 db.session.commit()_232 return redirect_to('properties')_232_232 return view('property_new', form)_232_232_232@app.route('/reservations/', methods=["POST"], defaults={'property_id': None})_232@app.route('/reservations/<property_id>', methods=["GET", "POST"])_232@login_required_232def new_reservation(property_id):_232 vacation_property = None_232 form = ReservationForm()_232 form.property_id.data = property_id_232_232 if request.method == 'POST':_232 if form.validate_on_submit():_232 guest = User.query.get(current_user.get_id())_232_232 vacation_property = VacationProperty.query.get(form.property_id.data)_232 reservation = Reservation(form.message.data, vacation_property, guest)_232 db.session.add(reservation)_232 db.session.commit()_232_232 reservation.notify_host()_232_232 return redirect_to('properties')_232_232 if property_id is not None:_232 vacation_property = VacationProperty.query.get(property_id)_232_232 return view_with_params('reservation', vacation_property=vacation_property, form=form)_232_232_232@app.route('/reservations', methods=["GET"])_232@login_required_232def reservations():_232 user = User.query.get(current_user.get_id())_232_232 reservations_as_host = Reservation.query \_232 .filter(VacationProperty.host_id == current_user.get_id() and len(VacationProperty.reservations) > 0) \_232 .join(VacationProperty) \_232 .filter(Reservation.vacation_property_id == VacationProperty.id) \_232 .all()_232_232 reservations_as_guest = user.reservations_232_232 return view_with_params('reservations',_232 reservations_as_guest=reservations_as_guest,_232 reservations_as_host=reservations_as_host)_232_232_232@app.route('/reservations/confirm', methods=["POST"])_232def confirm_reservation():_232 form = ReservationConfirmationForm()_232 sms_response_text = "Sorry, it looks like you don't have any reservations to respond to."_232_232 user = User.query.filter(User.phone_number == form.From.data).first()_232 reservation = Reservation \_232 .query \_232 .filter(Reservation.status == 'pending'_232 and Reservation.vacation_property.host.id == user.id) \_232 .first()_232_232 if reservation is not None:_232_232 if 'yes' in form.Body.data or 'accept' in form.Body.data:_232 reservation.confirm()_232 reservation.buy_number(user.area_code)_232 else:_232 reservation.reject()_232_232 db.session.commit()_232_232 sms_response_text = "You have successfully {0} the reservation".format(reservation.status)_232 reservation.notify_guest()_232_232 return twiml(_respond_message(sms_response_text))_232_232_232@app.route('/exchange/sms', methods=["POST"])_232def exchange_sms():_232 form = ExchangeForm()_232_232 outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)_232_232 response = MessagingResponse()_232 response.message(form.Body.data, to=outgoing_number)_232 return twiml(response)_232_232_232@app.route('/exchange/voice', methods=["POST"])_232def exchange_voice():_232 form = ExchangeForm()_232 outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)_232_232 response = VoiceResponse()_232 response.play("http://howtodocs.s3.amazonaws.com/howdy-tng.mp3")_232 response.dial(outgoing_number)_232 return twiml(response)_232_232_232# controller utils_232@app.before_request_232def before_request():_232 g.user = current_user_232 uri_pattern = request.url_rule_232 if current_user.is_authenticated and (_232 uri_pattern == '/' or uri_pattern == '/login' or uri_pattern == '/register'):_232 redirect_to('home')_232_232_232@login_manager.user_loader_232def load_user(id):_232 try:_232 return User.query.get(id)_232 except:_232 return None_232_232_232def _gather_outgoing_phone_number(incoming_phone_number, anonymous_phone_number):_232 reservation = Reservation.query \_232 .filter(Reservation.anonymous_phone_number == anonymous_phone_number) \_232 .first()_232_232 if reservation is None:_232 raise Exception('Reservation not found for {0}'.format(incoming_phone_number))_232_232 if reservation.guest.phone_number == incoming_phone_number:_232 return reservation.vacation_property.host.phone_number_232_232 return reservation.guest.phone_number_232_232_232def _respond_message(message):_232 response = MessagingResponse()_232 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.
_232from twilio.twiml.messaging_response import MessagingResponse_232from twilio.twiml.voice_response import VoiceResponse_232_232from airtng_flask import db, bcrypt, app, login_manager_232from flask import g, request_232from flask.ext.login import login_user, logout_user, current_user, login_required_232_232from airtng_flask.forms import RegisterForm, LoginForm, VacationPropertyForm, ReservationForm, \_232 ReservationConfirmationForm, ExchangeForm_232from airtng_flask.view_helpers import twiml, view, redirect_to, view_with_params_232from airtng_flask.models import init_models_module_232_232init_models_module(db, bcrypt, app)_232_232from airtng_flask.models.user import User_232from airtng_flask.models.vacation_property import VacationProperty_232from airtng_flask.models.reservation import Reservation_232_232_232@app.route('/', methods=["GET", "POST"])_232@app.route('/register', methods=["GET", "POST"])_232def register():_232 form = RegisterForm()_232 if request.method == 'POST':_232 if form.validate_on_submit():_232_232 if User.query.filter(User.email == form.email.data).count() > 0:_232 form.email.errors.append("Email address already in use.")_232 return view('register', form)_232_232 user = User(_232 name=form.name.data,_232 email=form.email.data,_232 password=form.password.data,_232 phone_number="+{0}{1}".format(form.country_code.data, form.phone_number.data),_232 area_code=str(form.phone_number.data)[0:3])_232_232 db.session.add(user)_232 db.session.commit()_232 login_user(user, remember=True)_232_232 return redirect_to('home')_232 else:_232 return view('register', form)_232_232 return view('register', form)_232_232_232@app.route('/login', methods=["GET", "POST"])_232def login():_232 form = LoginForm()_232 if request.method == 'POST':_232 if form.validate_on_submit():_232 candidate_user = User.query.filter(User.email == form.email.data).first()_232_232 if candidate_user is None or not bcrypt.check_password_hash(candidate_user.password,_232 form.password.data):_232 form.password.errors.append("Invalid credentials.")_232 return view('login', form)_232_232 login_user(candidate_user, remember=True)_232 return redirect_to('home')_232 return view('login', form)_232_232_232@app.route('/logout', methods=["POST"])_232@login_required_232def logout():_232 logout_user()_232 return redirect_to('home')_232_232_232@app.route('/home', methods=["GET"])_232@login_required_232def home():_232 return view('home')_232_232_232@app.route('/properties', methods=["GET"])_232@login_required_232def properties():_232 vacation_properties = VacationProperty.query.all()_232 return view_with_params('properties', vacation_properties=vacation_properties)_232_232_232@app.route('/properties/new', methods=["GET", "POST"])_232@login_required_232def new_property():_232 form = VacationPropertyForm()_232 if request.method == 'POST':_232 if form.validate_on_submit():_232 host = User.query.get(current_user.get_id())_232_232 property = VacationProperty(form.description.data, form.image_url.data, host)_232 db.session.add(property)_232 db.session.commit()_232 return redirect_to('properties')_232_232 return view('property_new', form)_232_232_232@app.route('/reservations/', methods=["POST"], defaults={'property_id': None})_232@app.route('/reservations/<property_id>', methods=["GET", "POST"])_232@login_required_232def new_reservation(property_id):_232 vacation_property = None_232 form = ReservationForm()_232 form.property_id.data = property_id_232_232 if request.method == 'POST':_232 if form.validate_on_submit():_232 guest = User.query.get(current_user.get_id())_232_232 vacation_property = VacationProperty.query.get(form.property_id.data)_232 reservation = Reservation(form.message.data, vacation_property, guest)_232 db.session.add(reservation)_232 db.session.commit()_232_232 reservation.notify_host()_232_232 return redirect_to('properties')_232_232 if property_id is not None:_232 vacation_property = VacationProperty.query.get(property_id)_232_232 return view_with_params('reservation', vacation_property=vacation_property, form=form)_232_232_232@app.route('/reservations', methods=["GET"])_232@login_required_232def reservations():_232 user = User.query.get(current_user.get_id())_232_232 reservations_as_host = Reservation.query \_232 .filter(VacationProperty.host_id == current_user.get_id() and len(VacationProperty.reservations) > 0) \_232 .join(VacationProperty) \_232 .filter(Reservation.vacation_property_id == VacationProperty.id) \_232 .all()_232_232 reservations_as_guest = user.reservations_232_232 return view_with_params('reservations',_232 reservations_as_guest=reservations_as_guest,_232 reservations_as_host=reservations_as_host)_232_232_232@app.route('/reservations/confirm', methods=["POST"])_232def confirm_reservation():_232 form = ReservationConfirmationForm()_232 sms_response_text = "Sorry, it looks like you don't have any reservations to respond to."_232_232 user = User.query.filter(User.phone_number == form.From.data).first()_232 reservation = Reservation \_232 .query \_232 .filter(Reservation.status == 'pending'_232 and Reservation.vacation_property.host.id == user.id) \_232 .first()_232_232 if reservation is not None:_232_232 if 'yes' in form.Body.data or 'accept' in form.Body.data:_232 reservation.confirm()_232 reservation.buy_number(user.area_code)_232 else:_232 reservation.reject()_232_232 db.session.commit()_232_232 sms_response_text = "You have successfully {0} the reservation".format(reservation.status)_232 reservation.notify_guest()_232_232 return twiml(_respond_message(sms_response_text))_232_232_232@app.route('/exchange/sms', methods=["POST"])_232def exchange_sms():_232 form = ExchangeForm()_232_232 outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)_232_232 response = MessagingResponse()_232 response.message(form.Body.data, to=outgoing_number)_232 return twiml(response)_232_232_232@app.route('/exchange/voice', methods=["POST"])_232def exchange_voice():_232 form = ExchangeForm()_232 outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)_232_232 response = VoiceResponse()_232 response.play("http://howtodocs.s3.amazonaws.com/howdy-tng.mp3")_232 response.dial(outgoing_number)_232 return twiml(response)_232_232_232# controller utils_232@app.before_request_232def before_request():_232 g.user = current_user_232 uri_pattern = request.url_rule_232 if current_user.is_authenticated and (_232 uri_pattern == '/' or uri_pattern == '/login' or uri_pattern == '/register'):_232 redirect_to('home')_232_232_232@login_manager.user_loader_232def load_user(id):_232 try:_232 return User.query.get(id)_232 except:_232 return None_232_232_232def _gather_outgoing_phone_number(incoming_phone_number, anonymous_phone_number):_232 reservation = Reservation.query \_232 .filter(Reservation.anonymous_phone_number == anonymous_phone_number) \_232 .first()_232_232 if reservation is None:_232 raise Exception('Reservation not found for {0}'.format(incoming_phone_number))_232_232 if reservation.guest.phone_number == incoming_phone_number:_232 return reservation.vacation_property.host.phone_number_232_232 return reservation.guest.phone_number_232_232_232def _respond_message(message):_232 response = MessagingResponse()_232 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.
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 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
_86from airtng_flask.models import app_db, auth_token, account_sid, phone_number, application_sid_86from flask import render_template_86from twilio.rest import Client_86_86DB = app_db()_86_86_86class Reservation(DB.Model):_86 __tablename__ = "reservations"_86_86 id = DB.Column(DB.Integer, primary_key=True)_86 message = DB.Column(DB.String, nullable=False)_86 status = DB.Column(DB.Enum('pending', 'confirmed', 'rejected', name='reservation_status_enum'),_86 default='pending')_86 anonymous_phone_number = DB.Column(DB.String, nullable=True)_86 guest_id = DB.Column(DB.Integer, DB.ForeignKey('users.id'))_86 vacation_property_id = DB.Column(DB.Integer, DB.ForeignKey('vacation_properties.id'))_86 guest = DB.relationship("User", back_populates="reservations")_86 vacation_property = DB.relationship("VacationProperty", back_populates="reservations")_86_86 def __init__(self, message, vacation_property, guest):_86 self.message = message_86 self.guest = guest_86 self.vacation_property = vacation_property_86 self.status = 'pending'_86_86 def confirm(self):_86 self.status = 'confirmed'_86_86 def reject(self):_86 self.status = 'rejected'_86_86 def __repr__(self):_86 return '<Reservation {0}>'.format(self.id)_86_86 def notify_host(self):_86 self._send_message(self.vacation_property.host.phone_number,_86 render_template('messages/sms_host.txt',_86 name=self.guest.name,_86 description=self.vacation_property.description,_86 message=self.message))_86_86 def notify_guest(self):_86 self._send_message(self.guest.phone_number,_86 render_template('messages/sms_guest.txt',_86 description=self.vacation_property.description,_86 status=self.status))_86_86 def buy_number(self, area_code):_86 numbers = self._get_twilio_client().available_phone_numbers("US") \_86 .local \_86 .list(area_code=area_code,_86 sms_enabled=True,_86 voice_enabled=True)_86_86 if numbers:_86 number = self._purchase_number(numbers[0])_86 self.anonymous_phone_number = number_86 return number_86 else:_86 numbers = self._get_twilio_client().available_phone_numbers("US") \_86 .local \_86 .list(sms_enabled=True, voice_enabled=True)_86_86 if numbers:_86 number = self._purchase_number(numbers[0])_86 self.anonymous_phone_number = number_86 return number_86_86 return None_86_86 def _purchase_number(self, number):_86 return self._get_twilio_client().incoming_phone_numbers \_86 .create(sms_application_sid=application_sid(),_86 voice_application_sid=application_sid(),_86 phone_number=number) \_86 .phone_number_86_86 def _get_twilio_client(self):_86 return Client(account_sid(), auth_token())_86_86 def _send_message(self, to, message):_86 self._get_twilio_client().messages \_86 .create(to,_86 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:
incoming_phone_number
number that originally called or sent an SMS.
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.
_232from twilio.twiml.messaging_response import MessagingResponse_232from twilio.twiml.voice_response import VoiceResponse_232_232from airtng_flask import db, bcrypt, app, login_manager_232from flask import g, request_232from flask.ext.login import login_user, logout_user, current_user, login_required_232_232from airtng_flask.forms import RegisterForm, LoginForm, VacationPropertyForm, ReservationForm, \_232 ReservationConfirmationForm, ExchangeForm_232from airtng_flask.view_helpers import twiml, view, redirect_to, view_with_params_232from airtng_flask.models import init_models_module_232_232init_models_module(db, bcrypt, app)_232_232from airtng_flask.models.user import User_232from airtng_flask.models.vacation_property import VacationProperty_232from airtng_flask.models.reservation import Reservation_232_232_232@app.route('/', methods=["GET", "POST"])_232@app.route('/register', methods=["GET", "POST"])_232def register():_232 form = RegisterForm()_232 if request.method == 'POST':_232 if form.validate_on_submit():_232_232 if User.query.filter(User.email == form.email.data).count() > 0:_232 form.email.errors.append("Email address already in use.")_232 return view('register', form)_232_232 user = User(_232 name=form.name.data,_232 email=form.email.data,_232 password=form.password.data,_232 phone_number="+{0}{1}".format(form.country_code.data, form.phone_number.data),_232 area_code=str(form.phone_number.data)[0:3])_232_232 db.session.add(user)_232 db.session.commit()_232 login_user(user, remember=True)_232_232 return redirect_to('home')_232 else:_232 return view('register', form)_232_232 return view('register', form)_232_232_232@app.route('/login', methods=["GET", "POST"])_232def login():_232 form = LoginForm()_232 if request.method == 'POST':_232 if form.validate_on_submit():_232 candidate_user = User.query.filter(User.email == form.email.data).first()_232_232 if candidate_user is None or not bcrypt.check_password_hash(candidate_user.password,_232 form.password.data):_232 form.password.errors.append("Invalid credentials.")_232 return view('login', form)_232_232 login_user(candidate_user, remember=True)_232 return redirect_to('home')_232 return view('login', form)_232_232_232@app.route('/logout', methods=["POST"])_232@login_required_232def logout():_232 logout_user()_232 return redirect_to('home')_232_232_232@app.route('/home', methods=["GET"])_232@login_required_232def home():_232 return view('home')_232_232_232@app.route('/properties', methods=["GET"])_232@login_required_232def properties():_232 vacation_properties = VacationProperty.query.all()_232 return view_with_params('properties', vacation_properties=vacation_properties)_232_232_232@app.route('/properties/new', methods=["GET", "POST"])_232@login_required_232def new_property():_232 form = VacationPropertyForm()_232 if request.method == 'POST':_232 if form.validate_on_submit():_232 host = User.query.get(current_user.get_id())_232_232 property = VacationProperty(form.description.data, form.image_url.data, host)_232 db.session.add(property)_232 db.session.commit()_232 return redirect_to('properties')_232_232 return view('property_new', form)_232_232_232@app.route('/reservations/', methods=["POST"], defaults={'property_id': None})_232@app.route('/reservations/<property_id>', methods=["GET", "POST"])_232@login_required_232def new_reservation(property_id):_232 vacation_property = None_232 form = ReservationForm()_232 form.property_id.data = property_id_232_232 if request.method == 'POST':_232 if form.validate_on_submit():_232 guest = User.query.get(current_user.get_id())_232_232 vacation_property = VacationProperty.query.get(form.property_id.data)_232 reservation = Reservation(form.message.data, vacation_property, guest)_232 db.session.add(reservation)_232 db.session.commit()_232_232 reservation.notify_host()_232_232 return redirect_to('properties')_232_232 if property_id is not None:_232 vacation_property = VacationProperty.query.get(property_id)_232_232 return view_with_params('reservation', vacation_property=vacation_property, form=form)_232_232_232@app.route('/reservations', methods=["GET"])_232@login_required_232def reservations():_232 user = User.query.get(current_user.get_id())_232_232 reservations_as_host = Reservation.query \_232 .filter(VacationProperty.host_id == current_user.get_id() and len(VacationProperty.reservations) > 0) \_232 .join(VacationProperty) \_232 .filter(Reservation.vacation_property_id == VacationProperty.id) \_232 .all()_232_232 reservations_as_guest = user.reservations_232_232 return view_with_params('reservations',_232 reservations_as_guest=reservations_as_guest,_232 reservations_as_host=reservations_as_host)_232_232_232@app.route('/reservations/confirm', methods=["POST"])_232def confirm_reservation():_232 form = ReservationConfirmationForm()_232 sms_response_text = "Sorry, it looks like you don't have any reservations to respond to."_232_232 user = User.query.filter(User.phone_number == form.From.data).first()_232 reservation = Reservation \_232 .query \_232 .filter(Reservation.status == 'pending'_232 and Reservation.vacation_property.host.id == user.id) \_232 .first()_232_232 if reservation is not None:_232_232 if 'yes' in form.Body.data or 'accept' in form.Body.data:_232 reservation.confirm()_232 reservation.buy_number(user.area_code)_232 else:_232 reservation.reject()_232_232 db.session.commit()_232_232 sms_response_text = "You have successfully {0} the reservation".format(reservation.status)_232 reservation.notify_guest()_232_232 return twiml(_respond_message(sms_response_text))_232_232_232@app.route('/exchange/sms', methods=["POST"])_232def exchange_sms():_232 form = ExchangeForm()_232_232 outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)_232_232 response = MessagingResponse()_232 response.message(form.Body.data, to=outgoing_number)_232 return twiml(response)_232_232_232@app.route('/exchange/voice', methods=["POST"])_232def exchange_voice():_232 form = ExchangeForm()_232 outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)_232_232 response = VoiceResponse()_232 response.play("http://howtodocs.s3.amazonaws.com/howdy-tng.mp3")_232 response.dial(outgoing_number)_232 return twiml(response)_232_232_232# controller utils_232@app.before_request_232def before_request():_232 g.user = current_user_232 uri_pattern = request.url_rule_232 if current_user.is_authenticated and (_232 uri_pattern == '/' or uri_pattern == '/login' or uri_pattern == '/register'):_232 redirect_to('home')_232_232_232@login_manager.user_loader_232def load_user(id):_232 try:_232 return User.query.get(id)_232 except:_232 return None_232_232_232def _gather_outgoing_phone_number(incoming_phone_number, anonymous_phone_number):_232 reservation = Reservation.query \_232 .filter(Reservation.anonymous_phone_number == anonymous_phone_number) \_232 .first()_232_232 if reservation is None:_232 raise Exception('Reservation not found for {0}'.format(incoming_phone_number))_232_232 if reservation.guest.phone_number == incoming_phone_number:_232 return reservation.vacation_property.host.phone_number_232_232 return reservation.guest.phone_number_232_232_232def _respond_message(message):_232 response = MessagingResponse()_232 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.
_232from twilio.twiml.messaging_response import MessagingResponse_232from twilio.twiml.voice_response import VoiceResponse_232_232from airtng_flask import db, bcrypt, app, login_manager_232from flask import g, request_232from flask.ext.login import login_user, logout_user, current_user, login_required_232_232from airtng_flask.forms import RegisterForm, LoginForm, VacationPropertyForm, ReservationForm, \_232 ReservationConfirmationForm, ExchangeForm_232from airtng_flask.view_helpers import twiml, view, redirect_to, view_with_params_232from airtng_flask.models import init_models_module_232_232init_models_module(db, bcrypt, app)_232_232from airtng_flask.models.user import User_232from airtng_flask.models.vacation_property import VacationProperty_232from airtng_flask.models.reservation import Reservation_232_232_232@app.route('/', methods=["GET", "POST"])_232@app.route('/register', methods=["GET", "POST"])_232def register():_232 form = RegisterForm()_232 if request.method == 'POST':_232 if form.validate_on_submit():_232_232 if User.query.filter(User.email == form.email.data).count() > 0:_232 form.email.errors.append("Email address already in use.")_232 return view('register', form)_232_232 user = User(_232 name=form.name.data,_232 email=form.email.data,_232 password=form.password.data,_232 phone_number="+{0}{1}".format(form.country_code.data, form.phone_number.data),_232 area_code=str(form.phone_number.data)[0:3])_232_232 db.session.add(user)_232 db.session.commit()_232 login_user(user, remember=True)_232_232 return redirect_to('home')_232 else:_232 return view('register', form)_232_232 return view('register', form)_232_232_232@app.route('/login', methods=["GET", "POST"])_232def login():_232 form = LoginForm()_232 if request.method == 'POST':_232 if form.validate_on_submit():_232 candidate_user = User.query.filter(User.email == form.email.data).first()_232_232 if candidate_user is None or not bcrypt.check_password_hash(candidate_user.password,_232 form.password.data):_232 form.password.errors.append("Invalid credentials.")_232 return view('login', form)_232_232 login_user(candidate_user, remember=True)_232 return redirect_to('home')_232 return view('login', form)_232_232_232@app.route('/logout', methods=["POST"])_232@login_required_232def logout():_232 logout_user()_232 return redirect_to('home')_232_232_232@app.route('/home', methods=["GET"])_232@login_required_232def home():_232 return view('home')_232_232_232@app.route('/properties', methods=["GET"])_232@login_required_232def properties():_232 vacation_properties = VacationProperty.query.all()_232 return view_with_params('properties', vacation_properties=vacation_properties)_232_232_232@app.route('/properties/new', methods=["GET", "POST"])_232@login_required_232def new_property():_232 form = VacationPropertyForm()_232 if request.method == 'POST':_232 if form.validate_on_submit():_232 host = User.query.get(current_user.get_id())_232_232 property = VacationProperty(form.description.data, form.image_url.data, host)_232 db.session.add(property)_232 db.session.commit()_232 return redirect_to('properties')_232_232 return view('property_new', form)_232_232_232@app.route('/reservations/', methods=["POST"], defaults={'property_id': None})_232@app.route('/reservations/<property_id>', methods=["GET", "POST"])_232@login_required_232def new_reservation(property_id):_232 vacation_property = None_232 form = ReservationForm()_232 form.property_id.data = property_id_232_232 if request.method == 'POST':_232 if form.validate_on_submit():_232 guest = User.query.get(current_user.get_id())_232_232 vacation_property = VacationProperty.query.get(form.property_id.data)_232 reservation = Reservation(form.message.data, vacation_property, guest)_232 db.session.add(reservation)_232 db.session.commit()_232_232 reservation.notify_host()_232_232 return redirect_to('properties')_232_232 if property_id is not None:_232 vacation_property = VacationProperty.query.get(property_id)_232_232 return view_with_params('reservation', vacation_property=vacation_property, form=form)_232_232_232@app.route('/reservations', methods=["GET"])_232@login_required_232def reservations():_232 user = User.query.get(current_user.get_id())_232_232 reservations_as_host = Reservation.query \_232 .filter(VacationProperty.host_id == current_user.get_id() and len(VacationProperty.reservations) > 0) \_232 .join(VacationProperty) \_232 .filter(Reservation.vacation_property_id == VacationProperty.id) \_232 .all()_232_232 reservations_as_guest = user.reservations_232_232 return view_with_params('reservations',_232 reservations_as_guest=reservations_as_guest,_232 reservations_as_host=reservations_as_host)_232_232_232@app.route('/reservations/confirm', methods=["POST"])_232def confirm_reservation():_232 form = ReservationConfirmationForm()_232 sms_response_text = "Sorry, it looks like you don't have any reservations to respond to."_232_232 user = User.query.filter(User.phone_number == form.From.data).first()_232 reservation = Reservation \_232 .query \_232 .filter(Reservation.status == 'pending'_232 and Reservation.vacation_property.host.id == user.id) \_232 .first()_232_232 if reservation is not None:_232_232 if 'yes' in form.Body.data or 'accept' in form.Body.data:_232 reservation.confirm()_232 reservation.buy_number(user.area_code)_232 else:_232 reservation.reject()_232_232 db.session.commit()_232_232 sms_response_text = "You have successfully {0} the reservation".format(reservation.status)_232 reservation.notify_guest()_232_232 return twiml(_respond_message(sms_response_text))_232_232_232@app.route('/exchange/sms', methods=["POST"])_232def exchange_sms():_232 form = ExchangeForm()_232_232 outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)_232_232 response = MessagingResponse()_232 response.message(form.Body.data, to=outgoing_number)_232 return twiml(response)_232_232_232@app.route('/exchange/voice', methods=["POST"])_232def exchange_voice():_232 form = ExchangeForm()_232 outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)_232_232 response = VoiceResponse()_232 response.play("http://howtodocs.s3.amazonaws.com/howdy-tng.mp3")_232 response.dial(outgoing_number)_232 return twiml(response)_232_232_232# controller utils_232@app.before_request_232def before_request():_232 g.user = current_user_232 uri_pattern = request.url_rule_232 if current_user.is_authenticated and (_232 uri_pattern == '/' or uri_pattern == '/login' or uri_pattern == '/register'):_232 redirect_to('home')_232_232_232@login_manager.user_loader_232def load_user(id):_232 try:_232 return User.query.get(id)_232 except:_232 return None_232_232_232def _gather_outgoing_phone_number(incoming_phone_number, anonymous_phone_number):_232 reservation = Reservation.query \_232 .filter(Reservation.anonymous_phone_number == anonymous_phone_number) \_232 .first()_232_232 if reservation is None:_232 raise Exception('Reservation not found for {0}'.format(incoming_phone_number))_232_232 if reservation.guest.phone_number == incoming_phone_number:_232 return reservation.vacation_property.host.phone_number_232_232 return reservation.guest.phone_number_232_232_232def _respond_message(message):_232 response = MessagingResponse()_232 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.
_232from twilio.twiml.messaging_response import MessagingResponse_232from twilio.twiml.voice_response import VoiceResponse_232_232from airtng_flask import db, bcrypt, app, login_manager_232from flask import g, request_232from flask.ext.login import login_user, logout_user, current_user, login_required_232_232from airtng_flask.forms import RegisterForm, LoginForm, VacationPropertyForm, ReservationForm, \_232 ReservationConfirmationForm, ExchangeForm_232from airtng_flask.view_helpers import twiml, view, redirect_to, view_with_params_232from airtng_flask.models import init_models_module_232_232init_models_module(db, bcrypt, app)_232_232from airtng_flask.models.user import User_232from airtng_flask.models.vacation_property import VacationProperty_232from airtng_flask.models.reservation import Reservation_232_232_232@app.route('/', methods=["GET", "POST"])_232@app.route('/register', methods=["GET", "POST"])_232def register():_232 form = RegisterForm()_232 if request.method == 'POST':_232 if form.validate_on_submit():_232_232 if User.query.filter(User.email == form.email.data).count() > 0:_232 form.email.errors.append("Email address already in use.")_232 return view('register', form)_232_232 user = User(_232 name=form.name.data,_232 email=form.email.data,_232 password=form.password.data,_232 phone_number="+{0}{1}".format(form.country_code.data, form.phone_number.data),_232 area_code=str(form.phone_number.data)[0:3])_232_232 db.session.add(user)_232 db.session.commit()_232 login_user(user, remember=True)_232_232 return redirect_to('home')_232 else:_232 return view('register', form)_232_232 return view('register', form)_232_232_232@app.route('/login', methods=["GET", "POST"])_232def login():_232 form = LoginForm()_232 if request.method == 'POST':_232 if form.validate_on_submit():_232 candidate_user = User.query.filter(User.email == form.email.data).first()_232_232 if candidate_user is None or not bcrypt.check_password_hash(candidate_user.password,_232 form.password.data):_232 form.password.errors.append("Invalid credentials.")_232 return view('login', form)_232_232 login_user(candidate_user, remember=True)_232 return redirect_to('home')_232 return view('login', form)_232_232_232@app.route('/logout', methods=["POST"])_232@login_required_232def logout():_232 logout_user()_232 return redirect_to('home')_232_232_232@app.route('/home', methods=["GET"])_232@login_required_232def home():_232 return view('home')_232_232_232@app.route('/properties', methods=["GET"])_232@login_required_232def properties():_232 vacation_properties = VacationProperty.query.all()_232 return view_with_params('properties', vacation_properties=vacation_properties)_232_232_232@app.route('/properties/new', methods=["GET", "POST"])_232@login_required_232def new_property():_232 form = VacationPropertyForm()_232 if request.method == 'POST':_232 if form.validate_on_submit():_232 host = User.query.get(current_user.get_id())_232_232 property = VacationProperty(form.description.data, form.image_url.data, host)_232 db.session.add(property)_232 db.session.commit()_232 return redirect_to('properties')_232_232 return view('property_new', form)_232_232_232@app.route('/reservations/', methods=["POST"], defaults={'property_id': None})_232@app.route('/reservations/<property_id>', methods=["GET", "POST"])_232@login_required_232def new_reservation(property_id):_232 vacation_property = None_232 form = ReservationForm()_232 form.property_id.data = property_id_232_232 if request.method == 'POST':_232 if form.validate_on_submit():_232 guest = User.query.get(current_user.get_id())_232_232 vacation_property = VacationProperty.query.get(form.property_id.data)_232 reservation = Reservation(form.message.data, vacation_property, guest)_232 db.session.add(reservation)_232 db.session.commit()_232_232 reservation.notify_host()_232_232 return redirect_to('properties')_232_232 if property_id is not None:_232 vacation_property = VacationProperty.query.get(property_id)_232_232 return view_with_params('reservation', vacation_property=vacation_property, form=form)_232_232_232@app.route('/reservations', methods=["GET"])_232@login_required_232def reservations():_232 user = User.query.get(current_user.get_id())_232_232 reservations_as_host = Reservation.query \_232 .filter(VacationProperty.host_id == current_user.get_id() and len(VacationProperty.reservations) > 0) \_232 .join(VacationProperty) \_232 .filter(Reservation.vacation_property_id == VacationProperty.id) \_232 .all()_232_232 reservations_as_guest = user.reservations_232_232 return view_with_params('reservations',_232 reservations_as_guest=reservations_as_guest,_232 reservations_as_host=reservations_as_host)_232_232_232@app.route('/reservations/confirm', methods=["POST"])_232def confirm_reservation():_232 form = ReservationConfirmationForm()_232 sms_response_text = "Sorry, it looks like you don't have any reservations to respond to."_232_232 user = User.query.filter(User.phone_number == form.From.data).first()_232 reservation = Reservation \_232 .query \_232 .filter(Reservation.status == 'pending'_232 and Reservation.vacation_property.host.id == user.id) \_232 .first()_232_232 if reservation is not None:_232_232 if 'yes' in form.Body.data or 'accept' in form.Body.data:_232 reservation.confirm()_232 reservation.buy_number(user.area_code)_232 else:_232 reservation.reject()_232_232 db.session.commit()_232_232 sms_response_text = "You have successfully {0} the reservation".format(reservation.status)_232 reservation.notify_guest()_232_232 return twiml(_respond_message(sms_response_text))_232_232_232@app.route('/exchange/sms', methods=["POST"])_232def exchange_sms():_232 form = ExchangeForm()_232_232 outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)_232_232 response = MessagingResponse()_232 response.message(form.Body.data, to=outgoing_number)_232 return twiml(response)_232_232_232@app.route('/exchange/voice', methods=["POST"])_232def exchange_voice():_232 form = ExchangeForm()_232 outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)_232_232 response = VoiceResponse()_232 response.play("http://howtodocs.s3.amazonaws.com/howdy-tng.mp3")_232 response.dial(outgoing_number)_232 return twiml(response)_232_232_232# controller utils_232@app.before_request_232def before_request():_232 g.user = current_user_232 uri_pattern = request.url_rule_232 if current_user.is_authenticated and (_232 uri_pattern == '/' or uri_pattern == '/login' or uri_pattern == '/register'):_232 redirect_to('home')_232_232_232@login_manager.user_loader_232def load_user(id):_232 try:_232 return User.query.get(id)_232 except:_232 return None_232_232_232def _gather_outgoing_phone_number(incoming_phone_number, anonymous_phone_number):_232 reservation = Reservation.query \_232 .filter(Reservation.anonymous_phone_number == anonymous_phone_number) \_232 .first()_232_232 if reservation is None:_232 raise Exception('Reservation not found for {0}'.format(incoming_phone_number))_232_232 if reservation.guest.phone_number == incoming_phone_number:_232 return reservation.vacation_property.host.phone_number_232_232 return reservation.guest.phone_number_232_232_232def _respond_message(message):_232 response = MessagingResponse()_232 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:
Create a seamless customer service experience by building an IVR Phone Tree for your company.
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.
Thanks for checking out this tutorial! If you have any feedback to share with us, we'd love to hear it. Tweet @twilio to let us know what you think.