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.
1from twilio.twiml.messaging_response import MessagingResponse2from twilio.twiml.voice_response import VoiceResponse34from airtng_flask import db, bcrypt, app, login_manager5from flask import g, request6from flask.ext.login import login_user, logout_user, current_user, login_required78from airtng_flask.forms import RegisterForm, LoginForm, VacationPropertyForm, ReservationForm, \9ReservationConfirmationForm, ExchangeForm10from airtng_flask.view_helpers import twiml, view, redirect_to, view_with_params11from airtng_flask.models import init_models_module1213init_models_module(db, bcrypt, app)1415from airtng_flask.models.user import User16from airtng_flask.models.vacation_property import VacationProperty17from airtng_flask.models.reservation import Reservation181920@app.route('/', methods=["GET", "POST"])21@app.route('/register', methods=["GET", "POST"])22def register():23form = RegisterForm()24if request.method == 'POST':25if form.validate_on_submit():2627if User.query.filter(User.email == form.email.data).count() > 0:28form.email.errors.append("Email address already in use.")29return view('register', form)3031user = User(32name=form.name.data,33email=form.email.data,34password=form.password.data,35phone_number="+{0}{1}".format(form.country_code.data, form.phone_number.data),36area_code=str(form.phone_number.data)[0:3])3738db.session.add(user)39db.session.commit()40login_user(user, remember=True)4142return redirect_to('home')43else:44return view('register', form)4546return view('register', form)474849@app.route('/login', methods=["GET", "POST"])50def login():51form = LoginForm()52if request.method == 'POST':53if form.validate_on_submit():54candidate_user = User.query.filter(User.email == form.email.data).first()5556if candidate_user is None or not bcrypt.check_password_hash(candidate_user.password,57form.password.data):58form.password.errors.append("Invalid credentials.")59return view('login', form)6061login_user(candidate_user, remember=True)62return redirect_to('home')63return view('login', form)646566@app.route('/logout', methods=["POST"])67@login_required68def logout():69logout_user()70return redirect_to('home')717273@app.route('/home', methods=["GET"])74@login_required75def home():76return view('home')777879@app.route('/properties', methods=["GET"])80@login_required81def properties():82vacation_properties = VacationProperty.query.all()83return view_with_params('properties', vacation_properties=vacation_properties)848586@app.route('/properties/new', methods=["GET", "POST"])87@login_required88def new_property():89form = VacationPropertyForm()90if request.method == 'POST':91if form.validate_on_submit():92host = User.query.get(current_user.get_id())9394property = VacationProperty(form.description.data, form.image_url.data, host)95db.session.add(property)96db.session.commit()97return redirect_to('properties')9899return view('property_new', form)100101102@app.route('/reservations/', methods=["POST"], defaults={'property_id': None})103@app.route('/reservations/<property_id>', methods=["GET", "POST"])104@login_required105def new_reservation(property_id):106vacation_property = None107form = ReservationForm()108form.property_id.data = property_id109110if request.method == 'POST':111if form.validate_on_submit():112guest = User.query.get(current_user.get_id())113114vacation_property = VacationProperty.query.get(form.property_id.data)115reservation = Reservation(form.message.data, vacation_property, guest)116db.session.add(reservation)117db.session.commit()118119reservation.notify_host()120121return redirect_to('properties')122123if property_id is not None:124vacation_property = VacationProperty.query.get(property_id)125126return view_with_params('reservation', vacation_property=vacation_property, form=form)127128129@app.route('/reservations', methods=["GET"])130@login_required131def reservations():132user = User.query.get(current_user.get_id())133134reservations_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()139140reservations_as_guest = user.reservations141142return view_with_params('reservations',143reservations_as_guest=reservations_as_guest,144reservations_as_host=reservations_as_host)145146147@app.route('/reservations/confirm', methods=["POST"])148def confirm_reservation():149form = ReservationConfirmationForm()150sms_response_text = "Sorry, it looks like you don't have any reservations to respond to."151152user = User.query.filter(User.phone_number == form.From.data).first()153reservation = Reservation \154.query \155.filter(Reservation.status == 'pending'156and Reservation.vacation_property.host.id == user.id) \157.first()158159if reservation is not None:160161if 'yes' in form.Body.data or 'accept' in form.Body.data:162reservation.confirm()163reservation.buy_number(user.area_code)164else:165reservation.reject()166167db.session.commit()168169sms_response_text = "You have successfully {0} the reservation".format(reservation.status)170reservation.notify_guest()171172return twiml(_respond_message(sms_response_text))173174175@app.route('/exchange/sms', methods=["POST"])176def exchange_sms():177form = ExchangeForm()178179outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)180181response = MessagingResponse()182response.message(form.Body.data, to=outgoing_number)183return twiml(response)184185186@app.route('/exchange/voice', methods=["POST"])187def exchange_voice():188form = ExchangeForm()189outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)190191response = VoiceResponse()192response.play("http://howtodocs.s3.amazonaws.com/howdy-tng.mp3")193response.dial(outgoing_number)194return twiml(response)195196197# controller utils198@app.before_request199def before_request():200g.user = current_user201uri_pattern = request.url_rule202if current_user.is_authenticated and (203uri_pattern == '/' or uri_pattern == '/login' or uri_pattern == '/register'):204redirect_to('home')205206207@login_manager.user_loader208def load_user(id):209try:210return User.query.get(id)211except:212return None213214215def _gather_outgoing_phone_number(incoming_phone_number, anonymous_phone_number):216reservation = Reservation.query \217.filter(Reservation.anonymous_phone_number == anonymous_phone_number) \218.first()219220if reservation is None:221raise Exception('Reservation not found for {0}'.format(incoming_phone_number))222223if reservation.guest.phone_number == incoming_phone_number:224return reservation.vacation_property.host.phone_number225226return reservation.guest.phone_number227228229def _respond_message(message):230response = MessagingResponse()231response.message(message)232return 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.
1from twilio.twiml.messaging_response import MessagingResponse2from twilio.twiml.voice_response import VoiceResponse34from airtng_flask import db, bcrypt, app, login_manager5from flask import g, request6from flask.ext.login import login_user, logout_user, current_user, login_required78from airtng_flask.forms import RegisterForm, LoginForm, VacationPropertyForm, ReservationForm, \9ReservationConfirmationForm, ExchangeForm10from airtng_flask.view_helpers import twiml, view, redirect_to, view_with_params11from airtng_flask.models import init_models_module1213init_models_module(db, bcrypt, app)1415from airtng_flask.models.user import User16from airtng_flask.models.vacation_property import VacationProperty17from airtng_flask.models.reservation import Reservation181920@app.route('/', methods=["GET", "POST"])21@app.route('/register', methods=["GET", "POST"])22def register():23form = RegisterForm()24if request.method == 'POST':25if form.validate_on_submit():2627if User.query.filter(User.email == form.email.data).count() > 0:28form.email.errors.append("Email address already in use.")29return view('register', form)3031user = User(32name=form.name.data,33email=form.email.data,34password=form.password.data,35phone_number="+{0}{1}".format(form.country_code.data, form.phone_number.data),36area_code=str(form.phone_number.data)[0:3])3738db.session.add(user)39db.session.commit()40login_user(user, remember=True)4142return redirect_to('home')43else:44return view('register', form)4546return view('register', form)474849@app.route('/login', methods=["GET", "POST"])50def login():51form = LoginForm()52if request.method == 'POST':53if form.validate_on_submit():54candidate_user = User.query.filter(User.email == form.email.data).first()5556if candidate_user is None or not bcrypt.check_password_hash(candidate_user.password,57form.password.data):58form.password.errors.append("Invalid credentials.")59return view('login', form)6061login_user(candidate_user, remember=True)62return redirect_to('home')63return view('login', form)646566@app.route('/logout', methods=["POST"])67@login_required68def logout():69logout_user()70return redirect_to('home')717273@app.route('/home', methods=["GET"])74@login_required75def home():76return view('home')777879@app.route('/properties', methods=["GET"])80@login_required81def properties():82vacation_properties = VacationProperty.query.all()83return view_with_params('properties', vacation_properties=vacation_properties)848586@app.route('/properties/new', methods=["GET", "POST"])87@login_required88def new_property():89form = VacationPropertyForm()90if request.method == 'POST':91if form.validate_on_submit():92host = User.query.get(current_user.get_id())9394property = VacationProperty(form.description.data, form.image_url.data, host)95db.session.add(property)96db.session.commit()97return redirect_to('properties')9899return view('property_new', form)100101102@app.route('/reservations/', methods=["POST"], defaults={'property_id': None})103@app.route('/reservations/<property_id>', methods=["GET", "POST"])104@login_required105def new_reservation(property_id):106vacation_property = None107form = ReservationForm()108form.property_id.data = property_id109110if request.method == 'POST':111if form.validate_on_submit():112guest = User.query.get(current_user.get_id())113114vacation_property = VacationProperty.query.get(form.property_id.data)115reservation = Reservation(form.message.data, vacation_property, guest)116db.session.add(reservation)117db.session.commit()118119reservation.notify_host()120121return redirect_to('properties')122123if property_id is not None:124vacation_property = VacationProperty.query.get(property_id)125126return view_with_params('reservation', vacation_property=vacation_property, form=form)127128129@app.route('/reservations', methods=["GET"])130@login_required131def reservations():132user = User.query.get(current_user.get_id())133134reservations_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()139140reservations_as_guest = user.reservations141142return view_with_params('reservations',143reservations_as_guest=reservations_as_guest,144reservations_as_host=reservations_as_host)145146147@app.route('/reservations/confirm', methods=["POST"])148def confirm_reservation():149form = ReservationConfirmationForm()150sms_response_text = "Sorry, it looks like you don't have any reservations to respond to."151152user = User.query.filter(User.phone_number == form.From.data).first()153reservation = Reservation \154.query \155.filter(Reservation.status == 'pending'156and Reservation.vacation_property.host.id == user.id) \157.first()158159if reservation is not None:160161if 'yes' in form.Body.data or 'accept' in form.Body.data:162reservation.confirm()163reservation.buy_number(user.area_code)164else:165reservation.reject()166167db.session.commit()168169sms_response_text = "You have successfully {0} the reservation".format(reservation.status)170reservation.notify_guest()171172return twiml(_respond_message(sms_response_text))173174175@app.route('/exchange/sms', methods=["POST"])176def exchange_sms():177form = ExchangeForm()178179outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)180181response = MessagingResponse()182response.message(form.Body.data, to=outgoing_number)183return twiml(response)184185186@app.route('/exchange/voice', methods=["POST"])187def exchange_voice():188form = ExchangeForm()189outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)190191response = VoiceResponse()192response.play("http://howtodocs.s3.amazonaws.com/howdy-tng.mp3")193response.dial(outgoing_number)194return twiml(response)195196197# controller utils198@app.before_request199def before_request():200g.user = current_user201uri_pattern = request.url_rule202if current_user.is_authenticated and (203uri_pattern == '/' or uri_pattern == '/login' or uri_pattern == '/register'):204redirect_to('home')205206207@login_manager.user_loader208def load_user(id):209try:210return User.query.get(id)211except:212return None213214215def _gather_outgoing_phone_number(incoming_phone_number, anonymous_phone_number):216reservation = Reservation.query \217.filter(Reservation.anonymous_phone_number == anonymous_phone_number) \218.first()219220if reservation is None:221raise Exception('Reservation not found for {0}'.format(incoming_phone_number))222223if reservation.guest.phone_number == incoming_phone_number:224return reservation.vacation_property.host.phone_number225226return reservation.guest.phone_number227228229def _respond_message(message):230response = MessagingResponse()231response.message(message)232return 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
1from airtng_flask.models import app_db, auth_token, account_sid, phone_number, application_sid2from flask import render_template3from twilio.rest import Client45DB = app_db()678class Reservation(DB.Model):9__tablename__ = "reservations"1011id = DB.Column(DB.Integer, primary_key=True)12message = DB.Column(DB.String, nullable=False)13status = DB.Column(DB.Enum('pending', 'confirmed', 'rejected', name='reservation_status_enum'),14default='pending')15anonymous_phone_number = DB.Column(DB.String, nullable=True)16guest_id = DB.Column(DB.Integer, DB.ForeignKey('users.id'))17vacation_property_id = DB.Column(DB.Integer, DB.ForeignKey('vacation_properties.id'))18guest = DB.relationship("User", back_populates="reservations")19vacation_property = DB.relationship("VacationProperty", back_populates="reservations")2021def __init__(self, message, vacation_property, guest):22self.message = message23self.guest = guest24self.vacation_property = vacation_property25self.status = 'pending'2627def confirm(self):28self.status = 'confirmed'2930def reject(self):31self.status = 'rejected'3233def __repr__(self):34return '<Reservation {0}>'.format(self.id)3536def notify_host(self):37self._send_message(self.vacation_property.host.phone_number,38render_template('messages/sms_host.txt',39name=self.guest.name,40description=self.vacation_property.description,41message=self.message))4243def notify_guest(self):44self._send_message(self.guest.phone_number,45render_template('messages/sms_guest.txt',46description=self.vacation_property.description,47status=self.status))4849def buy_number(self, area_code):50numbers = self._get_twilio_client().available_phone_numbers("US") \51.local \52.list(area_code=area_code,53sms_enabled=True,54voice_enabled=True)5556if numbers:57number = self._purchase_number(numbers[0])58self.anonymous_phone_number = number59return number60else:61numbers = self._get_twilio_client().available_phone_numbers("US") \62.local \63.list(sms_enabled=True, voice_enabled=True)6465if numbers:66number = self._purchase_number(numbers[0])67self.anonymous_phone_number = number68return number6970return None7172def _purchase_number(self, number):73return self._get_twilio_client().incoming_phone_numbers \74.create(sms_application_sid=application_sid(),75voice_application_sid=application_sid(),76phone_number=number) \77.phone_number7879def _get_twilio_client(self):80return Client(account_sid(), auth_token())8182def _send_message(self, to, message):83self._get_twilio_client().messages \84.create(to,85from_=phone_number(),86body=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.
1from twilio.twiml.messaging_response import MessagingResponse2from twilio.twiml.voice_response import VoiceResponse34from airtng_flask import db, bcrypt, app, login_manager5from flask import g, request6from flask.ext.login import login_user, logout_user, current_user, login_required78from airtng_flask.forms import RegisterForm, LoginForm, VacationPropertyForm, ReservationForm, \9ReservationConfirmationForm, ExchangeForm10from airtng_flask.view_helpers import twiml, view, redirect_to, view_with_params11from airtng_flask.models import init_models_module1213init_models_module(db, bcrypt, app)1415from airtng_flask.models.user import User16from airtng_flask.models.vacation_property import VacationProperty17from airtng_flask.models.reservation import Reservation181920@app.route('/', methods=["GET", "POST"])21@app.route('/register', methods=["GET", "POST"])22def register():23form = RegisterForm()24if request.method == 'POST':25if form.validate_on_submit():2627if User.query.filter(User.email == form.email.data).count() > 0:28form.email.errors.append("Email address already in use.")29return view('register', form)3031user = User(32name=form.name.data,33email=form.email.data,34password=form.password.data,35phone_number="+{0}{1}".format(form.country_code.data, form.phone_number.data),36area_code=str(form.phone_number.data)[0:3])3738db.session.add(user)39db.session.commit()40login_user(user, remember=True)4142return redirect_to('home')43else:44return view('register', form)4546return view('register', form)474849@app.route('/login', methods=["GET", "POST"])50def login():51form = LoginForm()52if request.method == 'POST':53if form.validate_on_submit():54candidate_user = User.query.filter(User.email == form.email.data).first()5556if candidate_user is None or not bcrypt.check_password_hash(candidate_user.password,57form.password.data):58form.password.errors.append("Invalid credentials.")59return view('login', form)6061login_user(candidate_user, remember=True)62return redirect_to('home')63return view('login', form)646566@app.route('/logout', methods=["POST"])67@login_required68def logout():69logout_user()70return redirect_to('home')717273@app.route('/home', methods=["GET"])74@login_required75def home():76return view('home')777879@app.route('/properties', methods=["GET"])80@login_required81def properties():82vacation_properties = VacationProperty.query.all()83return view_with_params('properties', vacation_properties=vacation_properties)848586@app.route('/properties/new', methods=["GET", "POST"])87@login_required88def new_property():89form = VacationPropertyForm()90if request.method == 'POST':91if form.validate_on_submit():92host = User.query.get(current_user.get_id())9394property = VacationProperty(form.description.data, form.image_url.data, host)95db.session.add(property)96db.session.commit()97return redirect_to('properties')9899return view('property_new', form)100101102@app.route('/reservations/', methods=["POST"], defaults={'property_id': None})103@app.route('/reservations/<property_id>', methods=["GET", "POST"])104@login_required105def new_reservation(property_id):106vacation_property = None107form = ReservationForm()108form.property_id.data = property_id109110if request.method == 'POST':111if form.validate_on_submit():112guest = User.query.get(current_user.get_id())113114vacation_property = VacationProperty.query.get(form.property_id.data)115reservation = Reservation(form.message.data, vacation_property, guest)116db.session.add(reservation)117db.session.commit()118119reservation.notify_host()120121return redirect_to('properties')122123if property_id is not None:124vacation_property = VacationProperty.query.get(property_id)125126return view_with_params('reservation', vacation_property=vacation_property, form=form)127128129@app.route('/reservations', methods=["GET"])130@login_required131def reservations():132user = User.query.get(current_user.get_id())133134reservations_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()139140reservations_as_guest = user.reservations141142return view_with_params('reservations',143reservations_as_guest=reservations_as_guest,144reservations_as_host=reservations_as_host)145146147@app.route('/reservations/confirm', methods=["POST"])148def confirm_reservation():149form = ReservationConfirmationForm()150sms_response_text = "Sorry, it looks like you don't have any reservations to respond to."151152user = User.query.filter(User.phone_number == form.From.data).first()153reservation = Reservation \154.query \155.filter(Reservation.status == 'pending'156and Reservation.vacation_property.host.id == user.id) \157.first()158159if reservation is not None:160161if 'yes' in form.Body.data or 'accept' in form.Body.data:162reservation.confirm()163reservation.buy_number(user.area_code)164else:165reservation.reject()166167db.session.commit()168169sms_response_text = "You have successfully {0} the reservation".format(reservation.status)170reservation.notify_guest()171172return twiml(_respond_message(sms_response_text))173174175@app.route('/exchange/sms', methods=["POST"])176def exchange_sms():177form = ExchangeForm()178179outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)180181response = MessagingResponse()182response.message(form.Body.data, to=outgoing_number)183return twiml(response)184185186@app.route('/exchange/voice', methods=["POST"])187def exchange_voice():188form = ExchangeForm()189outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)190191response = VoiceResponse()192response.play("http://howtodocs.s3.amazonaws.com/howdy-tng.mp3")193response.dial(outgoing_number)194return twiml(response)195196197# controller utils198@app.before_request199def before_request():200g.user = current_user201uri_pattern = request.url_rule202if current_user.is_authenticated and (203uri_pattern == '/' or uri_pattern == '/login' or uri_pattern == '/register'):204redirect_to('home')205206207@login_manager.user_loader208def load_user(id):209try:210return User.query.get(id)211except:212return None213214215def _gather_outgoing_phone_number(incoming_phone_number, anonymous_phone_number):216reservation = Reservation.query \217.filter(Reservation.anonymous_phone_number == anonymous_phone_number) \218.first()219220if reservation is None:221raise Exception('Reservation not found for {0}'.format(incoming_phone_number))222223if reservation.guest.phone_number == incoming_phone_number:224return reservation.vacation_property.host.phone_number225226return reservation.guest.phone_number227228229def _respond_message(message):230response = MessagingResponse()231response.message(message)232return 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.
1from twilio.twiml.messaging_response import MessagingResponse2from twilio.twiml.voice_response import VoiceResponse34from airtng_flask import db, bcrypt, app, login_manager5from flask import g, request6from flask.ext.login import login_user, logout_user, current_user, login_required78from airtng_flask.forms import RegisterForm, LoginForm, VacationPropertyForm, ReservationForm, \9ReservationConfirmationForm, ExchangeForm10from airtng_flask.view_helpers import twiml, view, redirect_to, view_with_params11from airtng_flask.models import init_models_module1213init_models_module(db, bcrypt, app)1415from airtng_flask.models.user import User16from airtng_flask.models.vacation_property import VacationProperty17from airtng_flask.models.reservation import Reservation181920@app.route('/', methods=["GET", "POST"])21@app.route('/register', methods=["GET", "POST"])22def register():23form = RegisterForm()24if request.method == 'POST':25if form.validate_on_submit():2627if User.query.filter(User.email == form.email.data).count() > 0:28form.email.errors.append("Email address already in use.")29return view('register', form)3031user = User(32name=form.name.data,33email=form.email.data,34password=form.password.data,35phone_number="+{0}{1}".format(form.country_code.data, form.phone_number.data),36area_code=str(form.phone_number.data)[0:3])3738db.session.add(user)39db.session.commit()40login_user(user, remember=True)4142return redirect_to('home')43else:44return view('register', form)4546return view('register', form)474849@app.route('/login', methods=["GET", "POST"])50def login():51form = LoginForm()52if request.method == 'POST':53if form.validate_on_submit():54candidate_user = User.query.filter(User.email == form.email.data).first()5556if candidate_user is None or not bcrypt.check_password_hash(candidate_user.password,57form.password.data):58form.password.errors.append("Invalid credentials.")59return view('login', form)6061login_user(candidate_user, remember=True)62return redirect_to('home')63return view('login', form)646566@app.route('/logout', methods=["POST"])67@login_required68def logout():69logout_user()70return redirect_to('home')717273@app.route('/home', methods=["GET"])74@login_required75def home():76return view('home')777879@app.route('/properties', methods=["GET"])80@login_required81def properties():82vacation_properties = VacationProperty.query.all()83return view_with_params('properties', vacation_properties=vacation_properties)848586@app.route('/properties/new', methods=["GET", "POST"])87@login_required88def new_property():89form = VacationPropertyForm()90if request.method == 'POST':91if form.validate_on_submit():92host = User.query.get(current_user.get_id())9394property = VacationProperty(form.description.data, form.image_url.data, host)95db.session.add(property)96db.session.commit()97return redirect_to('properties')9899return view('property_new', form)100101102@app.route('/reservations/', methods=["POST"], defaults={'property_id': None})103@app.route('/reservations/<property_id>', methods=["GET", "POST"])104@login_required105def new_reservation(property_id):106vacation_property = None107form = ReservationForm()108form.property_id.data = property_id109110if request.method == 'POST':111if form.validate_on_submit():112guest = User.query.get(current_user.get_id())113114vacation_property = VacationProperty.query.get(form.property_id.data)115reservation = Reservation(form.message.data, vacation_property, guest)116db.session.add(reservation)117db.session.commit()118119reservation.notify_host()120121return redirect_to('properties')122123if property_id is not None:124vacation_property = VacationProperty.query.get(property_id)125126return view_with_params('reservation', vacation_property=vacation_property, form=form)127128129@app.route('/reservations', methods=["GET"])130@login_required131def reservations():132user = User.query.get(current_user.get_id())133134reservations_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()139140reservations_as_guest = user.reservations141142return view_with_params('reservations',143reservations_as_guest=reservations_as_guest,144reservations_as_host=reservations_as_host)145146147@app.route('/reservations/confirm', methods=["POST"])148def confirm_reservation():149form = ReservationConfirmationForm()150sms_response_text = "Sorry, it looks like you don't have any reservations to respond to."151152user = User.query.filter(User.phone_number == form.From.data).first()153reservation = Reservation \154.query \155.filter(Reservation.status == 'pending'156and Reservation.vacation_property.host.id == user.id) \157.first()158159if reservation is not None:160161if 'yes' in form.Body.data or 'accept' in form.Body.data:162reservation.confirm()163reservation.buy_number(user.area_code)164else:165reservation.reject()166167db.session.commit()168169sms_response_text = "You have successfully {0} the reservation".format(reservation.status)170reservation.notify_guest()171172return twiml(_respond_message(sms_response_text))173174175@app.route('/exchange/sms', methods=["POST"])176def exchange_sms():177form = ExchangeForm()178179outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)180181response = MessagingResponse()182response.message(form.Body.data, to=outgoing_number)183return twiml(response)184185186@app.route('/exchange/voice', methods=["POST"])187def exchange_voice():188form = ExchangeForm()189outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)190191response = VoiceResponse()192response.play("http://howtodocs.s3.amazonaws.com/howdy-tng.mp3")193response.dial(outgoing_number)194return twiml(response)195196197# controller utils198@app.before_request199def before_request():200g.user = current_user201uri_pattern = request.url_rule202if current_user.is_authenticated and (203uri_pattern == '/' or uri_pattern == '/login' or uri_pattern == '/register'):204redirect_to('home')205206207@login_manager.user_loader208def load_user(id):209try:210return User.query.get(id)211except:212return None213214215def _gather_outgoing_phone_number(incoming_phone_number, anonymous_phone_number):216reservation = Reservation.query \217.filter(Reservation.anonymous_phone_number == anonymous_phone_number) \218.first()219220if reservation is None:221raise Exception('Reservation not found for {0}'.format(incoming_phone_number))222223if reservation.guest.phone_number == incoming_phone_number:224return reservation.vacation_property.host.phone_number225226return reservation.guest.phone_number227228229def _respond_message(message):230response = MessagingResponse()231response.message(message)232return 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.
1from twilio.twiml.messaging_response import MessagingResponse2from twilio.twiml.voice_response import VoiceResponse34from airtng_flask import db, bcrypt, app, login_manager5from flask import g, request6from flask.ext.login import login_user, logout_user, current_user, login_required78from airtng_flask.forms import RegisterForm, LoginForm, VacationPropertyForm, ReservationForm, \9ReservationConfirmationForm, ExchangeForm10from airtng_flask.view_helpers import twiml, view, redirect_to, view_with_params11from airtng_flask.models import init_models_module1213init_models_module(db, bcrypt, app)1415from airtng_flask.models.user import User16from airtng_flask.models.vacation_property import VacationProperty17from airtng_flask.models.reservation import Reservation181920@app.route('/', methods=["GET", "POST"])21@app.route('/register', methods=["GET", "POST"])22def register():23form = RegisterForm()24if request.method == 'POST':25if form.validate_on_submit():2627if User.query.filter(User.email == form.email.data).count() > 0:28form.email.errors.append("Email address already in use.")29return view('register', form)3031user = User(32name=form.name.data,33email=form.email.data,34password=form.password.data,35phone_number="+{0}{1}".format(form.country_code.data, form.phone_number.data),36area_code=str(form.phone_number.data)[0:3])3738db.session.add(user)39db.session.commit()40login_user(user, remember=True)4142return redirect_to('home')43else:44return view('register', form)4546return view('register', form)474849@app.route('/login', methods=["GET", "POST"])50def login():51form = LoginForm()52if request.method == 'POST':53if form.validate_on_submit():54candidate_user = User.query.filter(User.email == form.email.data).first()5556if candidate_user is None or not bcrypt.check_password_hash(candidate_user.password,57form.password.data):58form.password.errors.append("Invalid credentials.")59return view('login', form)6061login_user(candidate_user, remember=True)62return redirect_to('home')63return view('login', form)646566@app.route('/logout', methods=["POST"])67@login_required68def logout():69logout_user()70return redirect_to('home')717273@app.route('/home', methods=["GET"])74@login_required75def home():76return view('home')777879@app.route('/properties', methods=["GET"])80@login_required81def properties():82vacation_properties = VacationProperty.query.all()83return view_with_params('properties', vacation_properties=vacation_properties)848586@app.route('/properties/new', methods=["GET", "POST"])87@login_required88def new_property():89form = VacationPropertyForm()90if request.method == 'POST':91if form.validate_on_submit():92host = User.query.get(current_user.get_id())9394property = VacationProperty(form.description.data, form.image_url.data, host)95db.session.add(property)96db.session.commit()97return redirect_to('properties')9899return view('property_new', form)100101102@app.route('/reservations/', methods=["POST"], defaults={'property_id': None})103@app.route('/reservations/<property_id>', methods=["GET", "POST"])104@login_required105def new_reservation(property_id):106vacation_property = None107form = ReservationForm()108form.property_id.data = property_id109110if request.method == 'POST':111if form.validate_on_submit():112guest = User.query.get(current_user.get_id())113114vacation_property = VacationProperty.query.get(form.property_id.data)115reservation = Reservation(form.message.data, vacation_property, guest)116db.session.add(reservation)117db.session.commit()118119reservation.notify_host()120121return redirect_to('properties')122123if property_id is not None:124vacation_property = VacationProperty.query.get(property_id)125126return view_with_params('reservation', vacation_property=vacation_property, form=form)127128129@app.route('/reservations', methods=["GET"])130@login_required131def reservations():132user = User.query.get(current_user.get_id())133134reservations_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()139140reservations_as_guest = user.reservations141142return view_with_params('reservations',143reservations_as_guest=reservations_as_guest,144reservations_as_host=reservations_as_host)145146147@app.route('/reservations/confirm', methods=["POST"])148def confirm_reservation():149form = ReservationConfirmationForm()150sms_response_text = "Sorry, it looks like you don't have any reservations to respond to."151152user = User.query.filter(User.phone_number == form.From.data).first()153reservation = Reservation \154.query \155.filter(Reservation.status == 'pending'156and Reservation.vacation_property.host.id == user.id) \157.first()158159if reservation is not None:160161if 'yes' in form.Body.data or 'accept' in form.Body.data:162reservation.confirm()163reservation.buy_number(user.area_code)164else:165reservation.reject()166167db.session.commit()168169sms_response_text = "You have successfully {0} the reservation".format(reservation.status)170reservation.notify_guest()171172return twiml(_respond_message(sms_response_text))173174175@app.route('/exchange/sms', methods=["POST"])176def exchange_sms():177form = ExchangeForm()178179outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)180181response = MessagingResponse()182response.message(form.Body.data, to=outgoing_number)183return twiml(response)184185186@app.route('/exchange/voice', methods=["POST"])187def exchange_voice():188form = ExchangeForm()189outgoing_number = _gather_outgoing_phone_number(form.From.data, form.To.data)190191response = VoiceResponse()192response.play("http://howtodocs.s3.amazonaws.com/howdy-tng.mp3")193response.dial(outgoing_number)194return twiml(response)195196197# controller utils198@app.before_request199def before_request():200g.user = current_user201uri_pattern = request.url_rule202if current_user.is_authenticated and (203uri_pattern == '/' or uri_pattern == '/login' or uri_pattern == '/register'):204redirect_to('home')205206207@login_manager.user_loader208def load_user(id):209try:210return User.query.get(id)211except:212return None213214215def _gather_outgoing_phone_number(incoming_phone_number, anonymous_phone_number):216reservation = Reservation.query \217.filter(Reservation.anonymous_phone_number == anonymous_phone_number) \218.first()219220if reservation is None:221raise Exception('Reservation not found for {0}'.format(incoming_phone_number))222223if reservation.guest.phone_number == incoming_phone_number:224return reservation.vacation_property.host.phone_number225226return reservation.guest.phone_number227228229def _respond_message(message):230response = MessagingResponse()231response.message(message)232return 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.