File: //home/arjun/projects/buyercall_forms/buyercall/buyercall/blueprints/widgets/rest_api_twilio.py
import logging as log
import traceback
from datetime import datetime
from flask import (
Blueprint,
current_app as app,
request,
url_for,
)
from flask_cors import cross_origin
from twilio.twiml.voice_response import VoiceResponse
import redis
from buyercall.extensions import db, csrf
from buyercall.lib.util_lists import to_ints
from buyercall.lib.util_twilio import (
CallStorage, subaccount_client, update_response_time,
BusyAgents, InboundCallState as State
)
from .routing import (
get_agent_text,
schedule_retries,
Routing,
after_call_events,
)
from buyercall.blueprints.leads.models import Lead
from buyercall.blueprints.agents.models import Agent
DEFAULT_VOICE = 'alice'
DAYS = 86400 # The length of a day in seconds
twilio_api = Blueprint('twilio_api', __name__, template_folder='templates')
@twilio_api.route('/api/twiml/agent1/<int:lead_id>', methods=['POST'])
@csrf.exempt
def agent_digit_prompt(lead_id):
""" An agent is connected. Whisper the lead information to the agent, and
dial the lead.
"""
agent_id = request.args['agent_id']
log.debug('Agent {} picked up'.format(agent_id))
r = VoiceResponse()
# How do we greet the agent?
lead = Lead.query.join(Lead.widget).filter(Lead.id == lead_id).first()
agent_text = get_agent_text(lead.widget, lead)
if agent_text:
r.say(agent_text, voice=DEFAULT_VOICE)
# If digit prompt is disabled for the phone number, just continue
if not lead.widget.inbound.routing_config.get('digitPrompt', False):
r.redirect(url_for(
'twilio_api.twiml_agent_digit_press_result',
agent_id=agent_id,
lead_id=lead_id,
_external=True,
_scheme='https'
))
return str(r)
with r.gather(
action=url_for(
'twilio_api.twiml_agent_digit_press_result',
agent_id=agent_id,
lead_id=lead_id,
_external=True,
_scheme='https'
),
numDigits=1,
timeout=int(app.config.get('AMD_DIGIT_PROMPT_TIMEOUT', 5)),
finishOnKey=''
) as g:
g.say(
'Press any key to accept this call, or this call with be canceled.',
voice=DEFAULT_VOICE
)
r.say('This call will be canceled now.', voice=DEFAULT_VOICE)
return str(r)
@twilio_api.route(
'/api/twiml/agent2/<int:agent_id>/<int:lead_id>', methods=['POST'])
@csrf.exempt
def twiml_agent_digit_press_result(agent_id, lead_id):
# Declare redis url
redis_db = redis.StrictRedis(
host=app.config['REDIS_CONFIG_URL'],
port=app.config['REDIS_CONFIG_PORT'],
decode_responses=True
)
from .tasks import call_lead
digits = request.form.get('Digits', '')
log.debug('The agent pressed {}'.format(digits))
storage = CallStorage(redis_db, lead_id)
storage.state = State.AGENT_ONHOLD
lead = Lead.query.join(Lead.widget).filter(Lead.id == lead_id).first()
# import partnership information to get partnership id
from buyercall.blueprints.partnership.models import Partnership, PartnershipAccount
partner_account = PartnershipAccount.query \
.filter(PartnershipAccount.id == lead.partnership_account_id).first()
# Get the partner id to get relevant twilio credentails with twilio client
partner = Partnership.query.filter(Partnership.id == partner_account.partnership_id).first()
client = subaccount_client(storage.subaccount_sid, partner.id)
r = VoiceResponse()
called_agents_key = 'LIST{}'.format(lead_id)
with storage.lock():
if not storage.agent_id:
storage.agent_id = agent_id
calls = redis_db.lrange(called_agents_key, 0, -1)
if storage.agent_id != agent_id:
return str(r) # Someone else picked up; exit
# Cancel all other current calls
for call in calls:
a, sid = call.split('_')
if int(a) != agent_id:
log.info('Canceling call for agent {}...'.format(a))
client.calls(sid).update(status='completed')
lead.agent_id = agent_id
db.session.commit()
with r.dial() as d:
d.conference(
name='lead_%d' % lead_id,
muted=False,
beep=False,
startConferenceOnEnter=True,
endConferenceOnExit=True,
waitUrl=url_for(
'twilio_inbound.twiml_hold_music',
file_guid='loop.mp3',
_external=True,
_scheme='https'
),
waitMethod='GET'
)
call_lead.delay(lead_id, agent_id)
return str(r)
@twilio_api.route(
'/api/twiml/lead2/<int:lead_id>/<int:agent_id>', methods=['POST']
)
@csrf.exempt
def lead_greeting(lead_id, agent_id):
""" Greet the lead.
"""
lead = Lead.query.join(Lead.widget).filter(Lead.id == lead_id).first()
lang = lead.widget.options.get('language', 'en')
lead_text = lead.widget.options.get('whisperMessage', '')
r = VoiceResponse()
if lead_text:
r.say(lead_text, language=lang, voice=DEFAULT_VOICE)
r.redirect(url_for(
'twilio_api.lead_on_connect',
lead_id=lead_id,
agent_id=agent_id,
_external=True,
_scheme='https'
))
return str(r)
@twilio_api.route(
'/api/twiml/lead3/<int:lead_id>/<int:agent_id>', methods=['POST']
)
@csrf.exempt
def lead_on_connect(lead_id, agent_id):
""" An agent and the lead are both talking. Mark call as in-progress.
"""
# Declare redis url
redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])
redis_db.setex('CONNECT{}'.format(lead_id), 2 * DAYS, '1')
redis_db.setex('SUCC{}'.format(lead_id), 2 * DAYS, '1')
lead = Lead.query.filter(Lead.id == lead_id).first()
lead.agent_id = agent_id
lead.status = 'in-progress'
db.session.commit()
r = VoiceResponse()
with r.dial() as d:
d.conference(
name='lead_%d' % lead_id,
muted=False,
beep=True,
startConferenceOnEnter=True,
endConferenceOnExit=True,
waitUrl='',
waitMethod='GET',
record=lead.widget.options['recordCalls'],
statusCallback=url_for(
'twilio_api.lead_conference_callback',
lead_id=lead_id,
_external=True,
_scheme='https'
)
)
return str(r)
@twilio_api.route(
'/api/lead_conference_callback/<int:lead_id>', methods=['POST'])
@csrf.exempt
@cross_origin()
def lead_conference_callback(lead_id):
recording_url = request.form.get('RecordingUrl', '')
if not recording_url:
return
lead = Lead.query.filter(Lead.id == lead_id).first()
lead.recording_url = recording_url
db.session.commit()
return ''
@twilio_api.route(
'/api/lead_outbound_call_status/<int:lead_id>', methods=['POST'])
@csrf.exempt
@cross_origin()
def lead_outbound_call_status(lead_id):
""" Handle status of an attempt to initiate a call with a lead.
"""
# Declare redis url
redis_db = redis.StrictRedis(
host=app.config['REDIS_CONFIG_URL'],
port=app.config['REDIS_CONFIG_PORT'],
decode_responses=True
)
# The agent who's currently being called
call_status = request.form.get('CallStatus', '')
success = redis_db.get('SUCC{}'.format(lead_id))
r = VoiceResponse()
if call_status == 'completed' and success == '1':
lead = Lead.query.filter(Lead.id == lead_id).first()
lead.status = 'completed'
if lead.call_source == 'form':
vehicle_details = redis_db.get('CUSTOM_LEAD_MESSAGE: {}'.format(lead.id))
agent = Agent.query.filter(Agent.id == lead.agent_id).one()
if agent is not None:
lead.question += '{} - Agent: {} {}'.format(vehicle_details, agent.firstname, agent.lastname)
else:
lead.question += vehicle_details
db.session.add(lead)
db.session.commit()
try:
after_call_events(lead, lead.widget)
# send_notify_email(lead, lead.widget)
except Exception as e:
log.error(traceback.format_exc())
elif call_status in ['queued', 'ringing', 'in-progress']:
pass
elif call_status in ['busy', 'no-answer']:
r.say('The lead is not answering our call.', voice=DEFAULT_VOICE)
lead = Lead.query.filter(Lead.id == lead_id).first()
lead.status = 'unanswered'
db.session.add(lead)
db.session.commit()
elif call_status in ['failed', 'canceled']:
pass
return str(r)
@twilio_api.route(
'/api/agent_parallel_call_status/<int:agent_id>/<int:lead_id>',
methods=['POST']
)
@csrf.exempt
@cross_origin()
def agent_parallel_call_status(agent_id, lead_id):
""" Handle status updates when initiating calls with agents in parallel.
"""
# Declare redis url
redis_db = redis.StrictRedis(
host=app.config['REDIS_CONFIG_URL'],
port=app.config['REDIS_CONFIG_PORT']
)
# The agent who's currently being called
call_status = request.form.get('CallStatus', '')
answered_by = request.form.get('AnsweredBy', '')
if call_status == 'ringing':
update_response_time(call_status, agent_id, lead_id, redis_db)
return ''
if call_status == 'in-progress':
BusyAgents.add(agent_id)
update_response_time(call_status, agent_id, lead_id, redis_db)
return ''
if call_status == 'canceled':
# Someone else picked up
return ''
if call_status in ['initiated', 'queued']:
# The future is uncertain...
return ''
# The call has ended
BusyAgents.remove(agent_id)
if answered_by == 'machine':
call_status = 'no-answer'
log.info('Call update for agent {}: status is {}'.format(
agent_id, call_status
))
called_agents_key = 'LIST{}'.format(lead_id)
ongoing_calls = 1
storage = CallStorage(redis_db, lead_id)
with storage.lock():
calls = redis_db.lrange(called_agents_key, 0, -1)
for call in calls:
a, sid = call.decode().split('_')
if int(a) == agent_id:
log.info('Removing call for agent {} from list...'.format(a))
redis_db.lrem(called_agents_key, 1, call)
break
ongoing_calls = redis_db.llen(called_agents_key)
log.info('Still {} calls ongoing...'.format(ongoing_calls))
if ongoing_calls > 0:
return ''
call_duration = int(request.form.get('CallDuration', '0'))
from buyercall.blueprints.partnership.models import PartnershipAccount
lead = Lead.query.join(Lead.partnership_account).join(
PartnershipAccount.partnership
).filter(Lead.id == lead_id).first()
agent_update_lead(lead, agent_id, call_duration)
return '', 200
@twilio_api.route(
'/api/agent_sequential_call_status/<int:lead_id>', methods=['POST'])
@csrf.exempt
@cross_origin()
def agent_sequential_call_status(lead_id):
""" Handle status updates when initiating calls agents in sequence.
"""
# Declare redis url
redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])
# The agent who's currently being called
from buyercall.blueprints.partnership.models import PartnershipAccount, Partnership
agent_id = int(request.args['agent_id'])
call_status = request.form.get('CallStatus', '')
lead = Lead.query.join(Lead.partnership_account).join(
PartnershipAccount.partnership
).filter(Lead.id == lead_id).first()
partner_account = PartnershipAccount.query \
.filter(PartnershipAccount.id == lead.partnership_account_id).first()
# Get the partner id to get relevant twilio credentails with twilio client
partner = Partnership.query.filter(Partnership.id == partner_account.partnership_id).first()
answered_by = request.form.get('AnsweredBy', '')
if call_status in ['ringing', 'in-progress']:
update_response_time(call_status, agent_id, lead_id, redis_db)
if answered_by == 'machine':
call_status = 'no-answer'
storage = CallStorage(redis_db, lead_id)
client = subaccount_client(storage.subaccount_sid, partner.id)
r = Routing(client)
if call_status == 'queued' or call_status == 'ringing':
lead.status = call_status
db.session.commit()
return ''
elif call_status == 'in-progress':
# Save the agent id to the leads table
BusyAgents.add(agent_id)
lead.status = call_status
lead.agent_id = agent_id
lead.call_sid = request.form.get('CallSid', '')
db.session.commit()
return ''
elif call_status == 'completed':
BusyAgents.remove(agent_id)
# Save the agent id to the leads table
call_duration = int(request.form.get('CallDuration', '0'))
agent_update_lead(lead, agent_id, call_duration)
if redis_db.get('SUCC{}'.format(lead_id)) != '1':
agent_ids = to_ints(request.args['other_agents'])
r.route_sequential(agent_ids, lead.widget, lead)
return ''
else: # call_status in ['busy', 'failed', 'no-answer', 'canceled']:
BusyAgents.remove(agent_id)
agent_ids = to_ints(request.args['other_agents'])
r.route_sequential(agent_ids, lead.widget, lead)
return ''
@twilio_api.route('/api/record_voicemail/<int:lead_id>', methods=['POST'])
@csrf.exempt
@cross_origin()
def record_voicemail(lead_id):
recording_url = request.form.get('RecordingUrl', '')
lead = Lead.query.filter(Lead.id == lead_id).first()
lead.recording_url = recording_url
db.session.add(lead)
db.session.commit()
return '', 200
def agent_update_lead(lead, agent_id, call_duration):
""" Update the lead information at the end of a call.
"""
# Declare redis url
redis_db = redis.StrictRedis(
host=app.config['REDIS_CONFIG_URL'],
port=app.config['REDIS_CONFIG_PORT'],
decode_responses=True
)
success = (redis_db.get('SUCC{}'.format(lead.id)) == '1')
log.info('success is {}'.format(success))
if not success:
redis_db.setex('CONNECT{}'.format(lead.id), 2 * DAYS, '-1')
if success:
lead.status = 'completed'
elif lead.status == 'ringing':
lead.status = 'missed'
lead.agent_id = agent_id if success else None
lead.call_sid = request.form.get('CallSid', '')
lead.call_count += 1
lead.endtime = datetime.utcnow()
lead.duration = call_duration
if call_duration:
subscription = lead.partnership_account.subscription
if not subscription:
subscription = lead.partnership_account.partnership.subscription
subscription.update_usage(lead.partnership_account_id, seconds=call_duration)
db.session.commit()
if not success:
try:
after_call_events(lead, lead.widget)
# send_notify_email(lead, lead.widget)
except Exception as e:
log.error(traceback.format_exc())
schedule_retries(lead)