File: //home/arjun/projects/buyercall_forms/buyercall/buyercall/blueprints/widgets/bw_outbound.py
from __future__ import print_function
import time
import logging as log
import traceback
import json
from dateutil import parser
from datetime import datetime
from flask import (
Blueprint,
current_app,
request,
url_for,
)
# from flask_cors import cross_origin
# from twilio import twiml
import redis
from sqlalchemy import and_
from buyercall.lib.util_lists import to_ints
from buyercall.lib.util_webhooks import WebhookUtil
from buyercall.blueprints.widgets.routing import NoAgentsException
from buyercall.extensions import db, csrf
# from buyercall.lib.util_lists import to_ints
from buyercall.lib.util_twilio import (
BusyAgents,
CallStorage,
InboundCallState as State,
select_voice
)
from buyercall.lib.util_bandwidth import bw_client
from buyercall.lib.bandwidth_bxml import create_xml_response, Response
from buyercall.blueprints.widgets.routing import (
BandwidthRouting,
get_agent_text, send_notify_email, schedule_retries
)
from buyercall.blueprints.filters import format_phone_number
from buyercall.blueprints.leads.models import Lead
from buyercall.blueprints.agents.models import Agent
webhooker = WebhookUtil()
# FIXME: Default Bandwidth voice
DEFAULT_VOICE = 'susan'
DAYS = 86400 # The length of a day in seconds
MSG_AGENT_KEY_PRESS_PROMPT = "...Press any number key to accept the call or the hash key to cancel."
MSG_LEAD_NOT_ANSWERING = 'The lead is not answering our call.'
AGENT_TIMEOUT = 23
bw_outbound = Blueprint('bw_outbound', __name__, template_folder='templates')
@bw_outbound.route(
'/api/bw/outbound/agent/<int:lead_id>/<int:agent_id>',
methods=['POST']
)
@csrf.exempt
def agent_status_callback(lead_id, agent_id):
""" An agent is connected. Whisper the lead information to the agent, and
dial the lead.
"""
# Declare redis url
redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
port=current_app.config['REDIS_CONFIG_PORT'])
args = request.json
call_id = args.get('callId')
event_type = args.get('eventType')
tag = args.get('tag')
log.debug(args)
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()
storage = CallStorage(redis_db, lead_id)
client = bw_client(partner_account.partnership_id, 'voice')
call = client.calls[call_id]
call_info = client.calls.info(call_id)
language = lead.widget.options.get('language', 'en')
widget_digit_prompt = lead.widget.options.get('digitPrompt', True)
gender, locale, voice = select_voice(language)
if args.get('callState') == 'completed':
print('Agent call completed')
do_gather = False
if tag == 'start' and event_type == 'answer':
# update_response_time(call_status, agent_id, lead_id, redis_db)
log.debug('Agent {} picked up'.format(agent_id))
BusyAgents.add(agent_id)
agent_text = get_agent_text(lead.widget, lead)
storage.state = State.AGENT_ONHOLD
storage.routing_config = lead.widget.options
storage.agent_call_id = call_id
lead.agent_id = agent_id
lead.transfer_call_id = call_id
db.session.commit()
# If no whisper message, just continue
if not agent_text:
do_gather = True
else:
if lead.status != 'missed':
call.audio(
sentence=agent_text, tag='hidden_info',
gender=gender, locale=locale, voice=voice
)
do_call_lead = False
if do_gather or (
tag == 'hidden_info' and args.get('state') == 'PLAYBACK_STOP'
):
# If digit prompt is disabled for the phone number, just continue
if not (lead.widget.inbound.routing_config.get('digitPrompt', True)
or widget_digit_prompt):
do_call_lead = True
else:
call.gather({
"max_digits": 1,
"prompt": {
"sentence": MSG_AGENT_KEY_PRESS_PROMPT,
"gender": gender,
"locale": locale
},
"tag": 'gather',
})
if do_call_lead or (
tag == 'gather' and args.get('reason') == 'max-digits'
):
from .bw_tasks import bw_call_lead
# call.audio(sentence='Calling lead...')
called_agents_key = 'LIST{}'.format(lead_id)
with storage.lock():
if not storage.agent_id:
storage.agent_id = agent_id
if int(storage.agent_id) == agent_id:
# Cancel all other current calls
calls = redis_db.lrange(called_agents_key, 0, -1)
for call in calls:
call_decoded = call.decode()
a, sid = call_decoded.split('_')
if int(a) != agent_id:
log.info('Canceling call for agent {}...'.format(a))
client.calls.hangup(sid)
else:
# Someone else picked up; exit
log.debug('Agent {}: agent {} picked up; exit'.format(
agent_id, agent_id
))
return ''
try:
# Call lead
bw_call_lead.delay(lead_id, agent_id)
except Exception as e:
log.info('Agent id: {} cancel call during call up'
'date for call id: {} and exception is: {}'.format(agent_id, call_id, e))
if tag == 'hangup' and args.get('state') == 'PLAYBACK_STOP':
call.hangup()
if tag == 'gather' and args.get('reason') == 'inter-digit-timeout':
call.audio(
sentence="This call will be canceled now.", tag="canceling",
gender=gender, locale=locale, voice=voice
)
if args.get('state') == 'PLAYBACK_STOP' and (
tag == 'canceling' or tag == 'lead_call_error'
):
call.hangup()
if args.get('callState') == 'completed':
BusyAgents.remove(agent_id)
called_agents_key = 'LIST{}'.format(lead_id)
with storage.lock():
calls = redis_db.lrange(called_agents_key, 0, -1)
for call in calls:
call_decoded = call.decode()
a, sid = call_decoded.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))
retries_left = False
if storage.state == State.NEW and not ongoing_calls:
lead.status = 'missed'
db.session.commit()
retries_left = schedule_retries(lead)
if ongoing_calls == 0 and not retries_left:
try:
call_duration = int(call_info["chargeableDuration"])
except:
call_duration = 0
cause = args.get('cause')
lead.transfer_call_id = call_id
db.session.commit()
agent_update_lead(lead, agent_id, call_duration, cause)
try:
pass
client.calls.hangup(storage.call_sid)
except Exception as e:
log.info('Unable to hangup call with call id: {} because id does not exist. '
'Error: {}'.format(storage.agent_call_id, e))
return ''
@bw_outbound.route(
'/api/bw/outbound/agent_seq/<int:lead_id>/<int:agent_id>',
methods=['POST']
)
@csrf.exempt
def agent_status_callback_sequential(lead_id, agent_id):
""" Handle status updates when initiating calls agents in sequence.
"""
# Declare redis url
redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
port=current_app.config['REDIS_CONFIG_PORT'])
# The agent who's currently being called
agent_id = agent_id
args = request.json
call_id = args.get('callId')
event_type = args.get('eventType')
tag = args.get('tag')
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()
storage = CallStorage(redis_db, lead_id)
client = bw_client(partner_account.partnership_id)
call = client.calls[call_id]
call_info = client.calls.info(call_id)
language = lead.widget.options.get('language', 'en')
widget_digit_prompt = lead.widget.options.get('digitPrompt', True)
gender, locale, voice = select_voice(language)
do_gather = False
if tag == 'start' and event_type == 'answer':
# update_response_time(call_status, agent_id, lead_id, redis_db)
log.debug('Agent {} picked up'.format(agent_id))
BusyAgents.add(agent_id)
agent_text = get_agent_text(lead.widget, lead)
storage.state = State.AGENT_ONHOLD
storage.routing_config = lead.widget.options
storage.agent_call_id = call_id
lead.agent_id = agent_id
lead.transfer_call_id = call_id
db.session.commit()
# If no whisper message, just continue
if not agent_text:
do_gather = True
else:
if lead.status != 'missed':
call.audio(
sentence=agent_text, tag='hidden_info',
gender=gender, locale=locale, voice=voice
)
do_call_lead = False
if do_gather or (
tag == 'hidden_info' and args.get('state') == 'PLAYBACK_STOP'
):
# If digit prompt is disabled for the phone number, just continue
if not (lead.widget.inbound.routing_config.get('digitPrompt', True)
or widget_digit_prompt):
do_call_lead = True
else:
try:
call.gather({
"max_digits": 1,
"prompt": {
"sentence": MSG_AGENT_KEY_PRESS_PROMPT,
"gender": gender,
"locale": locale
},
"tag": 'gather',
})
except Exception as e:
log.error('An error occurred when trying '
'gather digit on sequence call: {}. Error: {}'.format(call_id, e))
if do_call_lead or (
tag == 'gather' and args.get('reason') == 'max-digits'
):
from .bw_tasks import bw_call_lead
try:
# Call lead
bw_call_lead.delay(lead_id, agent_id)
except Exception as e:
log.info('Agent id: {} cancel call during call up'
'date for call id: {} and exception is: {}'.format(agent_id, call_id, e))
if tag == 'hangup' and args.get('state') == 'PLAYBACK_STOP':
call.hangup()
if tag == 'gather' and args.get('reason') == 'inter-digit-timeout':
call.audio(
sentence="This call will be canceled now.", tag="canceling",
gender=gender, locale=locale, voice=voice
)
if args.get('state') == 'PLAYBACK_STOP' and (
tag == 'canceling' or tag == 'lead_call_error'
):
call.hangup()
if args.get('callState') == 'completed':
BusyAgents.remove(agent_id)
print('Agent call completed')
if len(request.args['other_agents']) > 0 and storage.state not in ['ONGOING', 'CAPTURED']:
agent_ids = to_ints(request.args['other_agents'])
BandwidthRouting(client).route_sequential(agent_ids, lead.widget, lead)
else:
try:
call_duration = int(call_info["chargeableDuration"])
except:
call_duration = 0
cause = args.get('cause')
lead.transfer_call_id = call_id
db.session.commit()
agent_update_lead(lead, agent_id, call_duration, cause)
if event_type == 'hangup':
try:
client.calls.hangup(storage.call_sid)
except Exception as e:
log.info('Unable to hangup call with call id: {} because id does not exist. '
'Error: {}'.format(storage.agent_call_id, e))
return ''
@bw_outbound.route(
'/api/bw/outbound/lead/<int:lead_id>/<int:agent_id>', methods=['POST']
)
@csrf.exempt
def lead_status_callback(lead_id, agent_id):
""" Greet the lead.
"""
# Declare redis url
redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
port=current_app.config['REDIS_CONFIG_PORT'])
args = request.json or request.args
print(args)
lead = Lead.query.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()
partner = Partnership.query.filter(Partnership.id == partner_account.partnership_id).first()
client = bw_client(partner.id, 'voice')
storage = CallStorage(redis_db, lead_id)
language = storage.routing_config.get('language', 'en')
gender, locale, voice = select_voice(language)
event_type = args.get('eventType')
call_id = args.get('callId')
call = client.calls[call_id]
if args.get('callState') == 'completed':
print("Lead completed")
if storage.state == State.AGENT_ONHOLD:
storage.state = State.MISSED
lead.status = 'missed'
db.session.commit()
# Stop dial tone
# agent_call_id_decoded = storage.agent_call_id.decode()
# client.calls[agent_call_id_decoded].audio(
# file_url=''
# )
# client.calls[agent_call_id_decoded].audio(
# sentence=MSG_LEAD_NOT_ANSWERING,
# tag='hangup',
# gender=gender, locale=locale, voice=voice
# )
elif storage.state == State.ONGOING:
storage.state = State.CAPTURED
lead.status = 'completed'
lead.call_sid = call_id
db.session.commit()
from buyercall.blueprints.phonenumbers.bw_tasks import bw_upload_recording
bw_upload_recording.delay(call_id)
# Hangup agent call
try:
client.calls.hangup(storage.agent_call_id.decode())
except Exception as e:
log.info('Unable to hangup call with call id: {}. '
'Error: {}'.format(storage.call_sid, e))
if redis_db.get('SUCC{}'.format(lead_id)) != '1':
# TODO: Call next agent
pass
return ''
if event_type == 'answer':
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', '')
call.audio(
sentence=lead_text,
gender=gender, locale=locale, voice=voice, tag='lead_whisper'
)
lead.status = 'in-progress'
db.session.commit()
if args.get('state') == 'PLAYBACK_STOP':
try:
client.calls[storage.agent_call_id.decode()].audio(
file_url=current_app.config['BEEP_SOUND']
)
except Exception as e:
log.error('Unable to play beep sound to '
'agent for call: {} Error: {}'.format(storage.agent_call_id.decode(), e))
lead = Lead.query.join(Lead.widget).filter(Lead.id == lead_id).first()
if lead.widget.options.get('recordCalls'):
try:
call.set(recording_enabled=True)
except Exception as e:
log.error('Unable to set recording on call: {} Error: {}'.format(call_id, e))
# recordings = call.recordings()
# if recordings:
# lead.recording_url = recordings[0].media
# db.session.commit()
agent_call_id_decoded = storage.agent_call_id.decode()
try:
client.bridge(call_ids=[call_id, agent_call_id_decoded])
except Exception as e:
client.calls.hangup(call_id)
log.info('The call bridge between calls: {} and {} could not be'
'completed. Error: {}'.format(call_id, agent_call_id_decoded, e))
storage.state = State.ONGOING
return ''
def agent_update_lead(lead, agent_id, call_duration, cause):
""" Update the lead information at the end of a call.
"""
# Declare redis url
redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
port=current_app.config['REDIS_CONFIG_PORT'])
storage = CallStorage(redis_db, lead.id)
success = (storage.state == State.CAPTURED)
log.info('success is {}'.format(success))
if success:
redis_db.setex('CONNECT{}'.format(lead.id), 2 * DAYS, '-1')
else:
redis_db.setex('CONNECT{}'.format(lead.id), 2 * DAYS, '1')
if success:
lead.status = 'completed'
elif lead.status == 'ringing':
lead.status = 'missed'
if storage.agent_outbound_call:
lead.agent_id = agent_id
else:
lead.agent_id = agent_id if success else None
lead.call_count += 1
lead.missed_call_cause = cause
lead.endtime = datetime.utcnow()
lead.duration = call_duration
if call_duration:
subscription = lead.partnership_account.subscription
subscription.update_usage(lead.partnership_account_id, seconds=call_duration)
db.session.commit()
try:
if not storage.agent_outbound_call:
send_notify_email(lead, lead.widget)
except Exception as e:
log.error(traceback.format_exc())
def agent_manual_outbound_call(lead, call_settings):
"""
agent_manual_outbound_call: Entry point for agent manual outbound calls. The api/call endpoint gets hit
when an outbound call is made through the application by an agent.
"""
# Declare redis url
redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
port=current_app.config['REDIS_CONFIG_PORT'])
# Initiate the redis storage used to store call details for handling call
storage = CallStorage(redis_db, lead.id)
storage.agent_outbound_call = True
# confirm the account's subscription. If not on account level then on partner level.
subscription = lead.partnership_account.subscription
if not subscription:
subscription = lead.partnership_account.partnership.subscription
storage.subaccount_sid = subscription.twilio_subaccount_sid
storage.clear_callback_cnt()
storage.state = State.NEW
# Send the first webhook saying that a incoming call started
webhooker.trigger_generic_webhook('operational_start_call', lead.id)
agent_id = call_settings['agents'][0]['id']
agent_call_method = call_settings['agents'][0]['contactUsing']
if not agent_id:
lead.status = 'missed'
db.session.commit()
raise NoAgentsException(lead)
route_to_agent(agent_id, agent_call_method, lead)
return lead
def route_to_agent(agent_id, agent_call_method, lead):
# Declare redis url
redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
port=current_app.config['REDIS_CONFIG_PORT'])
agent = Agent.query.filter(and_(
Agent.partnership_account_id == lead.partnership_account_id,
Agent.id == agent_id
)).first()
log.info('The lead call status is: {}'.format(lead.status))
if not agent_id or lead.status == 'completed':
log.error('No more agents available for lead {}.'.format(
lead.id
))
redis_db.setex('CONNECT{}'.format(lead.id), 2 * DAYS, '-1')
if lead.status != 'completed':
lead.status = 'missed'
db.session.commit()
return
# Reset the agent who answered
redis_db.setex('CALL{}'.format(lead.id), 4 * DAYS, '0')
log.debug('Trying call from {} to agent {}...'.format(
lead.inbound_id, agent_id
))
call_agent_outbound(
lead, agent, agent_call_method)
db.session.commit()
def call_agent_outbound(lead, agent, agent_call_method):
"""
For outbound calls when agent gets called first
"""
from_number = lead.inbound.phonenumber
call_agent_no(
agent,
agent_call_method,
answer_callback_url=url_for(
'bw_outbound.manual_outbound_agent_answer_webhook',
lead_id=lead.id,
agent_id=agent.id,
from_tn=from_number,
_external=True,
_scheme='https'
),
answer_fallback_callback_url=url_for(
'bw_outbound.manual_outbound_agent_answer_fallback_webhook',
lead_id=lead.id,
agent_id=agent.id,
from_tn=from_number,
_external=True,
_scheme='https'
),
disconnect_callback_url=url_for(
'bw_outbound.manual_outbound_agent_disconnect_webhook',
lead_id=lead.id,
agent_id=agent.id,
from_tn=from_number,
_external=True,
_scheme='https'
),
timeout=AGENT_TIMEOUT,
from_=from_number,
lead_id=lead.id,
)
return ''
def call_agent_no(agent, agent_call_method, answer_callback_url=None, disconnect_callback_url=None,
answer_fallback_callback_url=None, from_=None, lead_id=None, **kwargs):
""" Call either the agent's phone number, mobile
"""
from buyercall.blueprints.partnership.models import PartnershipAccount
partnership_account = PartnershipAccount.query\
.filter(PartnershipAccount.id == agent.partnership_account_id).first()
# Set the answering machine detecting callback
amd_callback_url = url_for(
'bw_outbound.manual_amd_webhook',
party='agent',
lead_id=lead_id,
_external=True,
_scheme='https'
)
amd_fallback_callback_url = url_for(
'bw_outbound.manual_amd_fallback_webhook',
party='agent',
lead_id=lead_id,
_external=True,
_scheme='https'
)
client = bw_client(partnership_account.partnership_id, 'voice')
to = None
try:
if agent_call_method == 'phone':
to = agent.phonenumber
elif agent_call_method == 'mobile':
to = agent.mobile
# Check to see if the agent number is a sip mobile number then use sip uri
from buyercall.blueprints.phonenumbers.models import Phone
buyercall_sip_lookup = Phone.mobile_sip_uri(to)
if buyercall_sip_lookup:
to = buyercall_sip_lookup
else:
to = format_phone_number(to)
agent_call = client.call.create(
c_from=from_,
c_to=to,
answer_callback_url=answer_callback_url,
disconnect_callback_url=disconnect_callback_url,
answer_fallback_callback_url=answer_fallback_callback_url,
tag='start-agent-outbound',
call_timeout=AGENT_TIMEOUT,
machine_detection={"callbackUrl": amd_callback_url,
"fallbackUrl": amd_fallback_callback_url,
"speechThreshold": 4,
"speechEndThreshold": 1,
"silenceTimeout": 120,
"detectionTimeout": 120,
}
)
log.error('The BW call API error is: {}'.format(agent_call))
except Exception as e:
log.info('Unable to make call to agent: {} with error: {}'.format(agent.id, e))
return ''
@bw_outbound.route('/api/bw/manual-outbound-agent-answer/webhook/<int:lead_id>/<int:agent_id>/<string:from_tn>', methods=['POST'])
@csrf.exempt
def manual_outbound_agent_answer_webhook(lead_id, agent_id, from_tn, *args):
""" The answer callback triggered when an agent answers the outbound the call
https://dev.bandwidth.com/voice/bxml/callbacks/answer.html
"""
# Declare redis url
redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
port=current_app.config['REDIS_CONFIG_PORT'])
# Activate and set redis storage and add lead
storage = CallStorage(redis_db, lead_id)
lead = Lead.query.filter(Lead.id == lead_id).first()
from buyercall.blueprints.partnership.models import PartnershipAccount
partnership_account = PartnershipAccount.query \
.filter(PartnershipAccount.id == lead.partnership_account_id).first()
# Fetch the request data. This includes message data for incoming sms/mms
args = request.json or request.args
agent_call_id = args.get('callId')
req_start_time = parser.parse(args.get('startTime', ''))
if req_start_time:
start_time = req_start_time
else:
start_time = parser.parse(args.get('enqueuedTime', ''))
connect_time = parser.parse(args.get('answerTime', ''))
storage.bridge_call_sid_a = agent_call_id
storage.manual_call = True
log.info('The manual outbound agent answer callback args: {}'.format(args))
# The save the call id to the db
lead.call_sid = agent_call_id
lead.call_count = 1
# Calculate response time by the agent to answer their phone
response_time_seconds = (connect_time - start_time).total_seconds()
lead.response_time_seconds = response_time_seconds
db.session.commit()
# Fire off webhook for start of call
from ..phonenumbers.bw_operational_tasks import delay_webhook_trigger
delay_webhook_trigger.apply_async(args=['operational_agent_call', lead.id], countdown=15)
# Declare the bandwidth response xml tag. This will be used for the xml responses
bxml = Response()
bxml.say('Please hold on while we try to connect you to the lead.')
bxml.ring('55')
client = bw_client(partnership_account.partnership_id, 'voice')
# The callbacks for the lead manual outbound calls
answer_callback_url = url_for(
'bw_outbound.manual_outbound_lead_answer_webhook',
lead_id=lead.id,
agent_call_id=agent_call_id,
_external=True,
_scheme='https'
)
answer_fallback_callback_url = url_for(
'bw_outbound.manual_outbound_lead_answer_fallback_webhook',
lead_id=lead.id,
agent_call_id=agent_call_id,
_external=True,
_scheme='https'
)
disconnect_callback_url = url_for(
'bw_outbound.manual_outbound_lead_disconnect_webhook',
lead_id=lead.id,
agent_call_id=agent_call_id,
_external=True,
_scheme='https'
)
# Set the answering machine detecting callback
amd_callback_url = url_for(
'bw_outbound.manual_amd_webhook',
party='lead',
lead_id=lead_id,
_external=True,
_scheme='https'
)
amd_fallback_callback_url = url_for(
'bw_outbound.manual_amd_fallback_webhook',
party='lead',
lead_id=lead_id,
_external=True,
_scheme='https'
)
# call the lead once the agent is connected.
try:
lead_call = client.call.create(
c_from=from_tn,
c_to=format_phone_number(lead.phonenumber),
answer_callback_url=answer_callback_url,
disconnect_callback_url=disconnect_callback_url,
answer_fallback_callback_url=answer_fallback_callback_url,
tag='start-lead-outbound',
call_timeout='60',
machine_detection={"callbackUrl": amd_callback_url,
"fallbackUrl": amd_fallback_callback_url,
"speechThreshold": 5,
"speechEndThreshold": 2,
"silenceTimeout": 10,
"detectionTimeout": 15,
}
)
json_call_string = json.loads(lead_call)
lead_call_id = json_call_string.get('callId', '')
storage.bridge_call_sid_b = lead_call_id
except Exception as e:
log.info('Unable to make call to lead: {} with error: {}'.format(lead_id, e))
return create_xml_response(bxml)
@bw_outbound.route('/api/bw/manual-outbound-agent-answer-fallback/webhook/<int:lead_id>/<int:agent_id>/<string:from_tn>', methods=['POST'])
@csrf.exempt
def manual_outbound_agent_answer_fallback_webhook(lead_id, agent_id, from_tn):
""" The answer callback triggered when an agent answers the outbound the call
https://dev.bandwidth.com/voice/bxml/callbacks/answer.html
"""
# Get the params of the Request
args = request.json or request.args
log.info('The manual outbound agent answer fallback callback response: {}'.format(args))
return manual_outbound_agent_answer_webhook(lead_id, agent_id, from_tn, args)
@bw_outbound.route('/api/bw/manual-outbound-agent-disconnect/webhook/<int:lead_id>/<int:agent_id>', methods=['POST'])
@csrf.exempt
def manual_outbound_agent_disconnect_webhook(lead_id, agent_id):
""" The disconnect callback triggered when an agent disconnect the outbound the call
https://dev.bandwidth.com/voice/bxml/callbacks/disconnect.html
"""
# Declare redis url
redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
port=current_app.config['REDIS_CONFIG_PORT'])
storage = CallStorage(redis_db, lead_id)
lead = Lead.query.filter(Lead.id == lead_id).first()
# Fetch the request data. This includes message data for incoming sms/mms
args = request.json or request.args
call_id = args.get('callId', '')
cause = args.get('cause', '')
error_message = args.get('errorMessage', '')
log.info('The manual outbound agent disconnect answer callback args: {}'.format(args))
if cause in ['timeout', 'cancel', 'rejected', 'busy']:
lead.call_sid = call_id
lead.status = 'missed'
lead.call_count = 1
lead.missed_call_cause = cause
lead.cause_description = error_message
db.session.commit()
# Fire off webhook for start of call
from ..phonenumbers.bw_operational_tasks import delay_webhook_trigger
delay_webhook_trigger.apply_async(args=['operational_end_call', lead.id], countdown=15)
elif cause == 'hangup' and storage.state not in ['ONGOING', 'MACHINE']:
if storage.bridge_call_sid_b:
from buyercall.blueprints.partnership.models import PartnershipAccount
partnership_account = PartnershipAccount.query \
.filter(PartnershipAccount.id == lead.partnership_account_id).first()
client = bw_client(partnership_account.partnership_id, 'voice')
client.call.update(storage.bridge_call_sid_b, 'completed')
lead.cause_description = 'Canceled or hangup by agent before lead could answer.'
db.session.commit()
return ''
@bw_outbound.route('/api/bw/widget-outbound-agent-answer/webhook/<int:lead_id>/<int:agent_id>/<string:from_tn>', methods=['POST'])
@csrf.exempt
def widget_outbound_agent_answer_webhook(lead_id, agent_id, from_tn, *args):
""" The answer callback triggered when an agent answers the widget outbound the call
https://dev.bandwidth.com/voice/bxml/callbacks/answer.html
"""
# Declare redis url
redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
port=current_app.config['REDIS_CONFIG_PORT'])
# Activate and set redis storage and add lead
storage = CallStorage(redis_db, lead_id)
lead = Lead.query.filter(Lead.id == lead_id).first()
from buyercall.blueprints.partnership.models import PartnershipAccount
partnership_account = PartnershipAccount.query \
.filter(PartnershipAccount.id == lead.partnership_account_id).first()
# Fetch the request data. This includes message data for incoming sms/mms
args = request.json or request.args
agent_call_id = args.get('callId', '')
req_start_time = parser.parse(args.get('startTime', ''))
if req_start_time:
start_time = req_start_time
else:
start_time = parser.parse(args.get('enqueuedTime', ''))
connect_time = parser.parse(args.get('answerTime', ''))
storage.bridge_call_sid_a = agent_call_id
log.info('The widget outbound agent answer callback args: {}'.format(args))
# The save the call id to the db
lead.call_sid = agent_call_id
lead.call_count = 1
storage.state = State.AGENT_ONHOLD
storage.routing_config = lead.widget.options
storage.agent_call_id = agent_call_id
storage.manual_call = False
lead.agent_id = agent_id
BusyAgents.add(agent_id)
# Calculate response time by the agent to answer their phone
response_time_seconds = (connect_time - start_time).total_seconds()
lead.response_time_seconds = response_time_seconds
db.session.commit()
log.debug('Agent {} picked up'.format(agent_id))
agent_text = get_agent_text(lead.widget, lead)
# Declare the bandwidth response xml tag. This will be used for the xml responses
bxml = Response()
widget_digit_prompt = lead.widget.options.get('digitPrompt', False)
# If no whisper message, just continue
if not agent_text:
bxml.say('Please hold on while we try to connect you to the lead.')
else:
if lead.status != 'missed':
bxml.say(agent_text)
if widget_digit_prompt:
# If digit prompt is disabled for the phone number, just continue
log.info('the widget digit prompt is: {}'.format(widget_digit_prompt))
# This is the gathering digit call web-hook. Every time there's a new event with the gather verb
# this endpoint will be hit with some call-back information
digits_webhook_url = url_for(
'bw_outbound.agent_acceptance_phone_call_digits_bw_webhook', lead_id=lead_id, agent_id=agent_id,
_external=True, _scheme='https')
digits_fallback_webhook_url = url_for(
'bw_outbound.agent_acceptance_phone_call_fallback_digits_bw_webhook', lead_id=lead_id,
agent_id=agent_id, _external=True, _scheme='https')
digit_gather = bxml.gather(digits_webhook_url, digits_fallback_webhook_url, tag='accept-gather-digits',
first_digit_timeout='5', max_digits='1', repeat_count='2')
digit_gather.say(MSG_AGENT_KEY_PRESS_PROMPT)
else:
bxml.ring('55')
bxml.tag('agent-answered')
called_agents_key = 'LIST{}'.format(lead_id)
calls = redis_db.lrange(called_agents_key, 0, -1)
log.info('list of calls: {}'.format(calls))
with storage.lock():
if not storage.agent_id:
storage.agent_id = agent_id
# Fire off webhook for start of call
from ..phonenumbers.bw_operational_tasks import delay_webhook_trigger
delay_webhook_trigger.apply_async(args=['operational_agent_call', lead.id], countdown=15)
if int(storage.agent_id) == agent_id:
# Cancel all other current calls
calls = redis_db.lrange(called_agents_key, 0, -1)
for call in calls:
call_decoded = call.decode()
a, sid = call_decoded.split('_')
if int(a) != agent_id:
log.info('Cancel call for agent {}....'.format(a))
client = bw_client(partnership_account.partnership_id, 'voice')
client.call.update(sid, 'completed')
else:
# Someone else picked up; exit
log.debug('Agent {}: agent {} picked up; exit'.format(
agent_id, agent_id
))
cancel_redirect_url = url_for(
'bw_outbound.agent_cancel_call_bw_webhook',
_external=True,
_scheme='https'
)
cancel_redirect_fallback_url = url_for(
'bw_outbound.agent_cancel_call_fallback_bw_webhook',
_external=True,
_scheme='https'
)
client = bw_client(partnership_account.partnership_id, 'voice')
client.call.update(agent_call_id, 'active', cancel_redirect_url, cancel_redirect_fallback_url,
'another-agent-picked-ip')
from buyercall.blueprints.partnership.models import PartnershipAccount
partnership_account = PartnershipAccount.query \
.filter(PartnershipAccount.id == lead.partnership_account_id).first()
client = bw_client(partnership_account.partnership_id, 'voice')
# The callbacks for the lead manual outbound calls
answer_callback_url = url_for(
'bw_outbound.manual_outbound_lead_answer_fallback_webhook',
lead_id=lead.id,
agent_call_id=agent_call_id,
_external=True,
_scheme='https'
)
answer_fallback_callback_url = url_for(
'bw_outbound.manual_outbound_lead_answer_fallback_webhook',
lead_id=lead.id,
agent_call_id=agent_call_id,
_external=True,
_scheme='https'
)
disconnect_callback_url = url_for(
'bw_outbound.manual_outbound_lead_disconnect_webhook',
lead_id=lead.id,
agent_call_id=agent_call_id,
_external=True,
_scheme='https'
)
# Set the answering machine detecting callback
amd_callback_url = url_for(
'bw_outbound.manual_amd_webhook',
party='lead',
lead_id=lead_id,
_external=True,
_scheme='https'
)
amd_fallback_callback_url = url_for(
'bw_outbound.manual_amd_fallback_webhook',
party='lead',
lead_id=lead_id,
_external=True,
_scheme='https'
)
# call the lead once the agent is connected.
try:
lead_call = client.call.create(
c_from=from_tn,
c_to=format_phone_number(lead.phonenumber),
answer_callback_url=answer_callback_url,
disconnect_callback_url=disconnect_callback_url,
answer_fallback_callback_url=answer_fallback_callback_url,
tag='start-lead-outbound',
call_timeout='60',
machine_detection={"callbackUrl": amd_callback_url,
"fallbackUrl": amd_fallback_callback_url,
"speechThreshold": 6,
"speechEndThreshold": 2,
"silenceTimeout": 10,
"detectionTimeout": 15,
}
)
json_call_string = json.loads(lead_call)
lead_call_id = json_call_string.get('callId', '')
storage.bridge_call_sid_b = lead_call_id
except Exception as e:
log.info('Unable to make call to lead: {} with error: {}'.format(lead_id, e))
return create_xml_response(bxml)
@bw_outbound.route('/api/bw/widget-outbound-agent-answer-fallback/webhook/<int:lead_id>/<int:agent_id>/<string:from_tn>', methods=['POST'])
@csrf.exempt
def widget_outbound_agent_answer_fallback_webhook(lead_id, agent_id, from_tn):
""" The answer callback triggered when an agent answers the widget outbound the call
https://dev.bandwidth.com/voice/bxml/callbacks/answer.html
"""
# Get the params of the Request
args = request.json or request.args
log.info('The widget outbound agent answer fallback callback response: {}'.format(args))
return widget_outbound_agent_answer_webhook(lead_id, agent_id, from_tn, args)
@bw_outbound.route('/api/bw/widget-parallel-outbound-agent-disconnect/webhook/<int:lead_id>/<int:agent_id>', methods=['POST'])
@csrf.exempt
def widget_parallel_outbound_agent_disconnect_webhook(lead_id, agent_id):
""" The disconnect callback triggered when an agent disconnect the widget parallel outbound the call
https://dev.bandwidth.com/voice/bxml/callbacks/disconnect.html
"""
# Declare redis url
redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
port=current_app.config['REDIS_CONFIG_PORT'])
storage = CallStorage(redis_db, lead_id)
lead = Lead.query.filter(Lead.id == lead_id).first()
# Fetch the request data. This includes message data for incoming sms/mms
args = request.json or request.args
call_id = args.get('callId', '')
cause = args.get('cause', '')
error_message = args.get('errorMessage', '')
log.info('The widget outbound agent disconnect answer callback args: {}'.format(args))
BusyAgents.remove(agent_id)
called_agents_key = 'LIST{}'.format(lead_id)
with storage.lock():
calls = redis_db.lrange(called_agents_key, 0, -1)
log.info('The calls are: {}'.format(calls))
for call in calls:
call_decoded = call.decode()
a, sid = call_decoded.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 cause in ['timeout', 'cancel', 'rejected', 'busy'] and storage.state != 'AGENT_ONHOLD':
lead.call_sid = call_id
lead.status = 'missed'
lead.call_count = 1
lead.missed_call_cause = cause
lead.cause_description = error_message
db.session.commit()
log.info('Still {} calls ongoing show me...'.format(ongoing_calls))
if ongoing_calls == 0:
schedule_retries(lead)
lead.call_count += 1
if not storage.agent_disconnect_webhook_sent:
# Fire off webhook for start of call
from ..phonenumbers.bw_operational_tasks import delay_webhook_trigger
delay_webhook_trigger.apply_async(args=['operational_end_call', lead.id], countdown=15)
storage.agent_disconnect_webhook_sent = True
if lead.status != 'retry-pending' and not schedule_retries(lead):
send_notify_email(lead, lead.widget)
elif cause == 'hangup' and storage.state not in ['ONGOING', 'MACHINE', 'AGENT_ONHOLD']:
log.info('THE STORAGE STATE IS: {}'.format(storage.state))
if storage.bridge_call_sid_b:
from buyercall.blueprints.partnership.models import PartnershipAccount
partnership_account = PartnershipAccount.query \
.filter(PartnershipAccount.id == lead.partnership_account_id).first()
client = bw_client(partnership_account.partnership_id, 'voice')
client.call.update(storage.bridge_call_sid_b, 'completed')
lead.cause_description = 'Canceled or hangup by agent before lead could answer.'
if storage.state == 'DIGIT_MISSED':
lead.missed_call_cause = cause
lead.status = 'missed'
lead.cause_description = 'Agent pressed # or did not answer the call.'
if ongoing_calls == 0:
# Fire off webhook for start of call
from ..phonenumbers.bw_operational_tasks import delay_webhook_trigger
delay_webhook_trigger.apply_async(args=['operational_end_call', lead.id], countdown=15)
storage.agent_disconnect_webhook_sent = True
schedule_retries(lead)
lead.call_count += 1
log.info('The lead status is: {}'.format(lead.status))
if lead.status != 'retry-pending' and not schedule_retries(lead):
send_notify_email(lead, lead.widget)
db.session.commit()
return ''
@bw_outbound.route('/api/bw/widget-sequence-outbound-agent-disconnect/webhook/<int:lead_id>/<int:agent_id>', methods=['POST'])
@csrf.exempt
def widget_sequence_outbound_agent_disconnect_webhook(lead_id, agent_id):
""" The disconnect callback triggered when an agent disconnect the widget sequence outbound the call
https://dev.bandwidth.com/voice/bxml/callbacks/disconnect.html
"""
# Declare redis url
redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
port=current_app.config['REDIS_CONFIG_PORT'])
storage = CallStorage(redis_db, lead_id)
lead = Lead.query.filter(Lead.id == lead_id).first()
# Fetch the request data. This includes message data for incoming sms/mms
args = request.json
additional_args = request.args
call_id = args.get('callId', '')
cause = args.get('cause', '')
error_message = args.get('errorMessage', '')
log.info('The widget outbound agent disconnect answer callback args: {}'.format(args))
BusyAgents.remove(agent_id)
from buyercall.blueprints.partnership.models import PartnershipAccount
partnership_account = PartnershipAccount.query \
.filter(PartnershipAccount.id == lead.partnership_account_id).first()
client = bw_client(partnership_account.partnership_id, 'voice')
if cause in ['timeout', 'cancel', 'rejected', 'busy'] and storage.state != 'AGENT_ONHOLD':
lead.call_sid = call_id
lead.status = 'missed'
lead.call_count = 1
lead.missed_call_cause = cause
lead.cause_description = error_message
db.session.commit()
log.info('The storage state: {}'.format(storage.state))
log.info('agents list are: {}'.format(len(additional_args['other_agents'])))
if len(additional_args['other_agents']) > 0 and storage.state not in ['ONGOING', 'CAPTURED']:
log.info('This got hit.')
agent_ids = to_ints(request.args['other_agents'])
return BandwidthRouting(client).route_sequential(agent_ids, lead.widget, lead)
else:
if schedule_retries(lead):
lead.call_count += 1
# Fire off webhook for start of call
from ..phonenumbers.bw_operational_tasks import delay_webhook_trigger
delay_webhook_trigger.apply_async(args=['operational_end_call', lead.id], countdown=15)
storage.agent_disconnect_webhook_sent = True
if lead.status != 'retry-pending' and not schedule_retries(lead):
send_notify_email(lead, lead.widget)
elif cause == 'hangup' and storage.state not in ['ONGOING', 'MACHINE', 'AGENT_ONHOLD']:
log.info('THE STORAGE STATE IS: {}'.format(storage.state))
if storage.bridge_call_sid_b:
client.call.update(storage.bridge_call_sid_b, 'completed')
if len(request.args['other_agents']) > 0 and storage.state not in ['ONGOING', 'CAPTURED']:
agent_ids = to_ints(request.args['other_agents'])
BandwidthRouting(client).route_sequential(agent_ids, lead.widget, lead)
else:
lead.cause_description = 'Canceled or hangup by agent before lead could answer.'
if storage.state == 'DIGIT_MISSED':
lead.missed_call_cause = cause
lead.status = 'missed'
lead.cause_description = 'Agent pressed # or did not answer the call.'
# Fire off webhook for start of call
from ..phonenumbers.bw_operational_tasks import delay_webhook_trigger
delay_webhook_trigger.apply_async(args=['operational_end_call', lead.id], countdown=15)
storage.agent_disconnect_webhook_sent = True
schedule_retries(lead)
lead.call_count += 1
log.info('The lead status is: {}'.format(lead.status))
if lead.status != 'retry-pending' and not schedule_retries(lead):
send_notify_email(lead, lead.widget)
db.session.commit()
return ''
@bw_outbound.route('/api/bw/manual-outbound-lead-answer/webhook/<int:lead_id>/<string:agent_call_id>', methods=['POST'])
@csrf.exempt
def manual_outbound_lead_answer_webhook(lead_id, agent_call_id, *args):
""" The answer callback triggered when a lead answers the outbound the call
https://dev.bandwidth.com/voice/bxml/callbacks/answer.html
"""
# Declare redis url
redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
port=current_app.config['REDIS_CONFIG_PORT'])
lead = Lead.query.filter(Lead.id == lead_id).first()
# Activate and set redis storage and add lead
storage = CallStorage(redis_db, lead_id)
# Fetch the request data. This includes message data for incoming sms/mms
args = request.json or request.args
lead_call_id = args.get('callId')
req_start_time = parser.parse(args.get('startTime', ''))
if req_start_time:
start_time = req_start_time
else:
start_time = parser.parse(args.get('enqueuedTime', ''))
lead.starttime = start_time
lead.status = 'in-progress'
storage.state = State.ONGOING
lead.transfer_call_id = lead_call_id
storage.bridge_call_sid_b = lead_call_id
db.session.commit()
log.info('The manual outbound lead answer callback args: {}'.format(args))
# Declare the bandwidth response xml tag. This will be used for the xml responses
bxml = Response()
# If it's a widget call we might want to play a whisper message
if storage.manual_call:
log.info('This is an manual outbound call')
else:
log.info('This is an widget call')
lead_text = lead.widget.options.get('whisperMessage', '')
record_call = lead.widget.options.get('recordCalls', '')
transcribe_call = storage.routing_config.get('transcribeAnsweredCall', '')
if lead_text:
bxml.say(lead_text)
if record_call:
if record_call:
record_call_back_url = url_for(
'bw_operational.operational_call_record_bw_webhook', lead_id=lead_id,
_external=True, _scheme='https')
if transcribe_call:
transcribe = "true"
transcribe_call_back_url = url_for(
'bw_operational.operational_call_transcribe_bw_webhook', lead_id=lead_id,
_external=True, _scheme='https')
else:
transcribe = "false"
transcribe_call_back_url = ""
bxml.start_record(recording_available_url=record_call_back_url, transcribe=transcribe,
transcribe_available_url=transcribe_call_back_url, tag='start-recording')
# Bridge the agent and lead calls to talk to each other
bxml.bridge(agent_call_id)
return create_xml_response(bxml)
@bw_outbound.route('/api/bw/manual-outbound-lead-answer-fallback/webhook/<int:lead_id>/<string:agent_call_id>', methods=['POST'])
@csrf.exempt
def manual_outbound_lead_answer_fallback_webhook(lead_id, agent_call_id):
""" The answer callback triggered when a lead answers the outbound the call
https://dev.bandwidth.com/voice/bxml/callbacks/answer.html
"""
# Get the params of the Request
args = request.json or request.args
log.info('The manual outbound lead answer fallback callback response: {}'.format(args))
return manual_outbound_lead_answer_webhook(lead_id, agent_call_id, args)
@bw_outbound.route('/api/bw/manual-outbound-lead-disconnect/webhook/<int:lead_id>/<string:agent_call_id>', methods=['POST'])
@csrf.exempt
def manual_outbound_lead_disconnect_webhook(lead_id, agent_call_id):
""" The disconnect callback triggered when a lead disconnect the outbound the call
https://dev.bandwidth.com/voice/bxml/callbacks/disconnect.html
"""
# Declare redis url
redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
port=current_app.config['REDIS_CONFIG_PORT'])
# Activate and set redis storage and add lead
storage = CallStorage(redis_db, lead_id)
lead = Lead.query.filter(Lead.id == lead_id).first()
# Fetch the request data. This includes message data for incoming sms/mms
args = request.json or request.args
call_id = args.get('callId', '')
cause = args.get('cause', '')
error_message = args.get('errorMessage', '')
req_start_time = parser.parse(args.get('startTime', ''))
if req_start_time:
start_time = req_start_time
else:
start_time = parser.parse(args.get('enqueuedTime', ''))
end_time = parser.parse(args.get('endTime', ''))
log.info('The manual outbound lead disconnect answer callback args: {}'.format(args))
if cause in ['timeout', 'cancel', 'rejected', 'busy']:
lead.transfer_call_id = call_id
lead.status = 'missed'
lead.call_count = 1
lead.missed_call_cause = cause
if error_message:
lead.cause_description = error_message
else:
lead.cause_description = 'The call was not successful.'
elif cause in ['hangup'] and storage.state not in ['MACHINE']:
lead.transfer_call_id = call_id
lead.status = 'completed'
lead.missed_call_cause = cause
duration = (end_time - start_time).total_seconds()
lead.duration = duration
lead.cause_description = 'Call successfully connected and ended.'
lead.starttime = start_time
lead.endtime = end_time
elif cause in ['callback-error', 'invalid-bxml', 'application-error', 'account-limit',
'node-capacity-exceeded', 'error', 'unknown'] and storage.state not in ['MACHINE']:
lead.transfer_call_id = call_id
lead.status = 'error'
lead.call_count = 1
lead.missed_call_cause = cause
lead.cause_description = error_message
lead.endtime = end_time
else:
lead.transfer_call_id = call_id
lead.call_count = 1
db.session.commit()
if lead.widget:
record_call = lead.widget.options.get('recordCalls', '')
if not record_call:
# Fire off webhook for start of call
from ..phonenumbers.bw_operational_tasks import delay_webhook_trigger
delay_webhook_trigger.apply_async(args=['operational_end_call', lead.id], countdown=15)
send_notify_email(lead, lead.widget)
return ''
@bw_outbound.route('/api/bw/manual-amd/webhook/<string:party>/<int:lead_id>', methods=['POST'])
@csrf.exempt
def manual_amd_webhook(party, lead_id, *args):
""" The machine detection callback
https://dev.bandwidth.com/voice/bxml/callbacks/machineDetectionComplete.html
"""
# Declare redis url
redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
port=current_app.config['REDIS_CONFIG_PORT'])
# Activate and set redis storage and add lead
storage = CallStorage(redis_db, lead_id)
lead = Lead.query.filter(Lead.id == lead_id).first()
# Fetch the request data.
args = request.json or request.args
call_id = args.get('callId', '')
result = args['machineDetectionResult'].get('value', '')
log.info('The machine detection args are: {}'.format(args))
if result in ['answering-machine']:
if party == 'agent':
from buyercall.blueprints.partnership.models import PartnershipAccount
partnership_account = PartnershipAccount.query \
.filter(PartnershipAccount.id == lead.partnership_account_id).first()
client = bw_client(partnership_account.partnership_id, 'voice')
if storage.bridge_call_sid_b:
client.call.update(storage.bridge_call_sid_b, 'completed')
agent_call_info = client.call.info(call_id)
agent_call_state = agent_call_info.get('state', '')
if agent_call_state and agent_call_state != 'disconnected':
client.call.update(call_id, 'completed')
storage.state = State.MACHINE
called_agents_key = 'LIST{}'.format(lead_id)
ongoing_calls = redis_db.llen(called_agents_key)
if ongoing_calls == 0:
lead.status = 'missed'
lead.missed_call_cause = 'answering-machine'
lead.cause_description = 'Agent answering machine was reached.'
db.session.commit()
elif party == 'lead':
storage.state = State.MACHINE
lead.status = 'missed'
lead.missed_call_cause = 'answering-machine'
lead.cause_description = 'Lead answering machine was reached.'
db.session.commit()
else:
pass
return ''
@bw_outbound.route('/api/bw/manual-amd-fallback/webhook/<string:party>/<int:lead_id>', methods=['POST'])
@csrf.exempt
def manual_amd_fallback_webhook(party, lead_id):
""" The machine detection fallback callback
https://dev.bandwidth.com/voice/bxml/callbacks/machineDetectionComplete.html
"""
# Fetch the request data.
args = request.json or request.args
log.info('The machine detection fallback args are: {}'.format(args))
return manual_amd_webhook(party, lead_id, args)
@bw_outbound.route('/api/bw/agent-acceptance-phone-call-digits/webhook/<int:lead_id>/<int:agent_id>',
methods=['GET', 'POST'])
@csrf.exempt
def agent_acceptance_phone_call_digits_bw_webhook(lead_id, agent_id, *args):
# Declare redis config url
redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'], port=current_app.config['REDIS_CONFIG_PORT'])
# Declare the redis db model
storage = CallStorage(redis_db, lead_id)
# Get the params of the Request
args = request.json or request.args
log.info('The operation call gather digits call-back/web-hook response: {}'.format(args))
agent_call_id = args.get('callId', '')
pressed_digit = args.get('digits', '')
terminating_digit = args.get('terminatingDigit', '')
lead = Lead.query.filter(Lead.id == lead_id).first()
# Declare the bandwidth response xml tag. This will be used for the xml responses
from_tn = lead.inbound.phonenumber
bxml = Response()
if pressed_digit not in ["#", ""]:
bxml.ring('55')
bxml.tag('agent-answered')
called_agents_key = 'LIST{}'.format(lead_id)
from buyercall.blueprints.partnership.models import PartnershipAccount
partnership_account = PartnershipAccount.query \
.filter(PartnershipAccount.id == lead.partnership_account_id).first()
client = bw_client(partnership_account.partnership_id, 'voice')
with storage.lock():
if not storage.agent_id:
storage.agent_id = agent_id
# Fire off webhook for start of call
from ..phonenumbers.bw_operational_tasks import delay_webhook_trigger
delay_webhook_trigger.apply_async(args=['operational_agent_call', lead.id], countdown=15)
if int(storage.agent_id) == agent_id:
# Cancel all other current calls
calls = redis_db.lrange(called_agents_key, 0, -1)
for call in calls:
call_decoded = call.decode()
a, sid = call_decoded.split('_')
if int(a) != agent_id:
log.info('Cancel call for agent {}....'.format(a))
client.call.update(sid, 'completed')
else:
# Someone else picked up; exit
log.debug('Agent {}: agent {} picked up; exit'.format(
agent_id, agent_id
))
cancel_redirect_url = url_for(
'bw_outbound.agent_cancel_call_bw_webhook',
_external=True,
_scheme='https'
)
cancel_redirect_fallback_url = url_for(
'bw_outbound.agent_cancel_call_fallback_bw_webhook',
_external=True,
_scheme='https'
)
client.call.update(agent_call_id, 'active', cancel_redirect_url, cancel_redirect_fallback_url,
'another-agent-picked-ip')
# The callbacks for the lead manual outbound calls
answer_callback_url = url_for(
'bw_outbound.manual_outbound_lead_answer_webhook',
lead_id=lead.id,
agent_call_id=agent_call_id,
_external=True,
_scheme='https'
)
answer_fallback_callback_url = url_for(
'bw_outbound.manual_outbound_lead_answer_fallback_webhook',
lead_id=lead.id,
agent_call_id=agent_call_id,
_external=True,
_scheme='https'
)
disconnect_callback_url = url_for(
'bw_outbound.manual_outbound_lead_disconnect_webhook',
lead_id=lead.id,
agent_call_id=agent_call_id,
_external=True,
_scheme='https'
)
# Set the answering machine detecting callback
amd_callback_url = url_for(
'bw_outbound.manual_amd_webhook',
party='lead',
lead_id=lead_id,
_external=True,
_scheme='https'
)
amd_fallback_callback_url = url_for(
'bw_outbound.manual_amd_fallback_webhook',
party='lead',
lead_id=lead_id,
_external=True,
_scheme='https'
)
# call the lead once the agent is connected.
try:
lead_call = client.call.create(
c_from=from_tn,
c_to=format_phone_number(lead.phonenumber),
answer_callback_url=answer_callback_url,
disconnect_callback_url=disconnect_callback_url,
answer_fallback_callback_url=answer_fallback_callback_url,
tag='start-lead-outbound',
call_timeout='60',
machine_detection={"callbackUrl": amd_callback_url,
"fallbackUrl": amd_fallback_callback_url,
"speechThreshold": 6,
"speechEndThreshold": 2,
"silenceTimeout": 10,
"detectionTimeout": 15,
}
)
json_call_string = json.loads(lead_call)
lead_call_id = json_call_string.get('callId', '')
storage.bridge_call_sid_b = lead_call_id
except Exception as e:
log.info('Unable to make call to lead: {} with error: {}'.format(lead_id, e))
else:
storage.state = State.DIGIT_MISSED
bxml.hangup()
return create_xml_response(bxml)
@bw_outbound.route('/api/bw/agent-acceptance-phone-call-digits-fallback/webhook/<int:lead_id>/<int:agent_id>',
methods=['GET', 'POST'])
@csrf.exempt
def agent_acceptance_phone_call_fallback_digits_bw_webhook(lead_id, agent_id):
""" This is the fallback callback url for the gather digits callback event
https://dev.bandwidth.com/voice/bxml/callbacks/gather.html
"""
# Get the params of the Request
args = request.json or request.args
log.info('The call gather digits fallback callback response: {}'.format(args))
return agent_acceptance_phone_call_digits_bw_webhook(lead_id, agent_id, args)
@bw_outbound.route('/api/bw/agent-cancel-call/webhook', methods=['GET', 'POST'])
@csrf.exempt
def agent_cancel_call_bw_webhook(*args):
""" This is the redirect callback url to cancel call when another agent picked up
https://dev.bandwidth.com/voice/bxml/callbacks/redirect.html
"""
# Get the params of the Request
args = request.json or request.args
log.info('The call cancel redirect callback response: {}'.format(args))
# Declare the bandwidth response xml tag. This will be used for the xml responses
bxml = Response()
bxml.say('Another agent answered this call. You will be disconnected.')
bxml.hangup()
return create_xml_response(bxml)
@bw_outbound.route('/api/bw/agent-cancel-fallback-call/webhook', methods=['GET', 'POST'])
@csrf.exempt
def agent_cancel_call_fallback_bw_webhook():
""" This is the redirect fallback callback url to cancel call when another agent picked up
https://dev.bandwidth.com/voice/bxml/callbacks/redirect.html
"""
# Get the params of the Request
args = request.json or request.args
log.info('The call cancel redirect fallback callback response: {}'.format(args))
return agent_cancel_call_bw_webhook(args)