File: //home/arjun/projects/buyercall_forms/buyercall/buyercall/blueprints/mobile/mobile_inbound_call.py
"""
Governs the logic of the incoming lead calls and lead/agent interations.
At any given moment the call is in one of the following states:
* 'NEW': New inbound call
* 'CALLBACK': Callback call, agent called first
* 'LEAD_ONHOLD': Lead selected a routing, placed on hold until agent answers
* 'AGENT_ONHOLD': In a scheduled callback, the agent is called first and placed
on hold.
* 'CALLBACK_PROMPT': Lead has been waiting for a while and is asked if he wants
to be called back.
* 'ONGOING': Agent answered/call ongoing.
* 'MISSED': Call ended/lead missed.
* 'CAPTURED': Call ended/lead captured.
"""
import logging
import traceback
from flask import (
Blueprint,
request,
current_app as app,
url_for,
jsonify,
make_response
)
from buyercall.lib.util_twilio import (
bw_client,
CallStorage,
InboundCallState as State
)
from buyercall.lib.bandwidth_bxml import create_xml_response, Response
import json
from buyercall.lib.util_webhooks import WebhookUtil
import redis
from dateutil import parser
from sqlalchemy import and_
from sqlalchemy.orm import load_only
from buyercall.blueprints.leads.models import Lead
from buyercall.blueprints.mobile.models import Endpoint
from buyercall.blueprints.phonenumbers.models import Phone, Audio
from buyercall.blueprints.agents.models import Agent
from buyercall.blueprints.block_numbers.models import Block
from buyercall.blueprints.contacts.models import Contact
from buyercall.blueprints.filters import format_phone_number_bracket, format_phone_number
from buyercall.extensions import csrf, db
from buyercall.lib.bandwidth import (
BandwidthException,
)
log = logging.getLogger(__name__)
webhooker = WebhookUtil()
mobile_call_inbound = Blueprint(
'mobile_call_inbound', __name__, template_folder='templates'
)
provider = "bandwidth"
TRANSFER_CALL_TIMEOUT = "40"
""" The call timeout period in seconds for transfer """
@mobile_call_inbound.route('/bw/mobile/voice', methods=['GET', 'POST'])
@csrf.exempt
def mobile_voice_call():
""" Entry point for the incoming and outbound mobile sip phone call. This is specifically for Bandwidth API V2.
This is for SIP Registrar calls.
"""
from buyercall.lib.util_bandwidth import authenticate_bw_request
authenticated = authenticate_bw_request(request.headers)
if authenticated:
# Fetch the request data. This includes message data for incoming sms/mms
args = request.json or request.args
log.info('The initiated mobile call callback args: {}'.format(args))
return handle_mobile_call(args)
else:
print('Authentication failed')
status_code = 401
message = jsonify(message='Authentication failed.')
response = make_response(message, status_code)
return response
def handle_mobile_call(*args):
"""
This function handles sip calling once a call is initiated. This
function will check if agent is available and then determine if its a inbound
or outbound call and transfer accordingly.
"""
args = request.json or request.args
c_from = args.get('from', '')
c_to = args.get('to', '')
call_id = args.get('callId', '')
# Declare BXML response
bxml = Response()
outbound_call = Endpoint.api_username_check(c_from)
inbound_call = False
if outbound_call:
bc_sip = Endpoint.query.filter(Endpoint.sip_username == c_from).first()
bc_sip_tn = Phone.query.filter(Phone.id == bc_sip.inbound_id).first()
lead_phone_number = c_to
else:
inbound_call = Endpoint.api_username_check(c_to)
if inbound_call:
bc_sip = Endpoint.query.filter(Endpoint.sip_username == c_to).first()
bc_sip_tn = Phone.query.filter(Phone.id == bc_sip.inbound_id).first()
else:
log.info('The to number is: {}'.format(c_to))
bc_sip_tn = Phone.query.filter(and_(Phone.phonenumber == c_to, Phone.is_deactivated.is_(False))).first()
log.info('The returned phone id is: {}'.format(bc_sip_tn.id))
if bc_sip_tn.id:
inbound_call = True
bc_sip = Endpoint.query.filter(Endpoint.inbound_id == bc_sip_tn.id).first()
else:
bxml.hangup()
log.error('Unable to find bc sip or number for to: {} and from: {}'.format(c_to, c_from))
return create_xml_response(bxml)
lead_phone_number = c_from
if not bc_sip:
bxml.hangup()
log.error('Unable to find bc sip or number for to: {} and from: {}'.format(c_to, c_from))
return create_xml_response(bxml)
# Check to see if a lead exist already
lead = Lead.query.filter(Lead.call_sid == call_id).first()
# Set the lead or create it if it doesn't exist yet
if lead:
lead_id = lead.id
else:
from .tasks import mobile_lead_create
lead_new = mobile_lead_create(bc_sip_tn.id, call_id, lead_phone_number, outbound_call)
lead_id = lead_new
# Ping webhook to say a new call has started
webhooker.trigger_generic_webhook('mobile_start_call', lead_id)
# Declare redis config url
redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])
# Activate and set redis storage and add lead
storage = CallStorage(redis_db, lead_id)
# Set the storage routing and sip id
storage.routing_config = bc_sip_tn.routing_config
storage.state = State.NEW
storage.sip_endpoint_id = bc_sip.id
storage.agent_id = bc_sip.agent_id
if outbound_call:
storage.sip_call_direction = 'outbound'
else:
storage.sip_call_direction = 'inbound'
if inbound_call:
# Retrieve the agent that's associated with the endpoint
agent = Agent.query.filter(Agent.id == bc_sip.agent_id).first()
# Check if the incoming call is labeled as a blocked number
is_number_blocked = Block.blocked(bc_sip_tn.id, lead_phone_number)
if is_number_blocked:
storage.state = State.BLOCKED
storage.cause_description = 'Incoming phone number on blocked list for incoming mobile call.'
bxml.hangup()
return create_xml_response(bxml)
if agent.available_now is False:
storage.cause_description = 'The agent is unavailable.'
return missed_call(storage, lead_id, bc_sip_tn.id)
# Inbound transfer
return mobile_inbound_transfer(lead_id, args)
else:
# outbound transfer
return mobile_outbound_transfer(lead_id, args)
def mobile_inbound_transfer(lead_id, args):
"""
This function handles inbound transfers for mobile
"""
# Declare redis config url
redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])
# Activate and set redis storage and add lead
storage = CallStorage(redis_db, lead_id)
# Declare the bandwidth response xml tag. This will be used for the xml responses
bxml = Response()
# Return the lead information
lead = Lead.query.filter(Lead.id == lead_id).first()
# Return all variables from the Routing saved in Redis Storage
greeting_enabled = storage.routing_config.get('greetingMessage', '')
greeting_type = storage.routing_config.get('whisperMessageType', '')
greeting = storage.routing_config.get('whisperMessage', '')
language = storage.routing_config.get('language', '')
# This is the web-hook that will be called if a transfer is answered
transfer_answer_webhook_url = url_for(
'mobile_call_inbound.mobile_transfer_answer_bw_webhook', lead_id=lead.id,
_external=True, _scheme='https')
# This is the fallback web-hook that will be called if a transfer is answered
transfer_answer_fallback_webhook_url = url_for(
'mobile_call_inbound.mobile_transfer_answer_fallback_bw_webhook', lead_id=lead.id,
_external=True, _scheme='https')
# This is the web-hook that will be called if a transfer is disconnected
transfer_disconnect_webhook_url = url_for(
'mobile_call_inbound.mobile_transfer_disconnect_bw_webhook', lead_id=lead.id,
_external=True, _scheme='https')
# This is the web-hook that will be called if a transfer is completed
transfer_complete_webhook_url = url_for(
'mobile_call_inbound.mobile_transfer_complete_bw_webhook', inbound_id=lead.inbound_id, lead_id=lead.id,
_external=True, _scheme='https')
# This is the fallback web-hook that will be called if a transfer is completed
transfer_complete_fallback_webhook_url = url_for(
'mobile_call_inbound.mobile_transfer_complete_fallback_bw_webhook', inbound_id=lead.inbound_id, lead_id=lead.id,
_external=True, _scheme='https')
try:
# If greeting is enabled and its the very first agent in sequence and not a retry call
# we play a greeting message to the caller
if greeting_enabled:
if greeting_type == 'audio':
greeting_audio = Audio.query \
.filter(and_(Audio.whisper_message_type == 'whisperMessage',
Audio.inbound_id == lead.inbound_id)).first()
if greeting_audio:
greeting_audio_link = greeting_audio.audio_url
bxml.tag('pre-transfer')
bxml.play_audio(greeting_audio_link)
else:
log.error('There is no whisperMessage audio link for inbound id: {}'.format(inbound_id))
else:
if language == 'es':
bxml.tag('pre-transfer')
bxml.say(greeting, 'female', 'es_MX', 'esperanza')
else:
bxml.tag('pre-transfer')
bxml.say(greeting)
call_time_out = TRANSFER_CALL_TIMEOUT
log.info(f"storage call sid id before saving : {storage.call_sid} {type(storage.call_sid)}")
# Check if the caller id needs to be the tracking number or the caller's number
caller_id = lead.phonenumber
# Perform the transfer to the agent mobile sip
transfer = bxml.transfer(caller_id, call_time_out, transfer_complete_webhook_url,
transfer_complete_fallback_webhook_url, tag='transfer-initiated')
# Lookup to see if the agent number is associated with a BuyerCall sip endpoint
bc_sip_lookup = Phone.mobile_sip_uri(lead.my_phone)
if bc_sip_lookup:
agent_call_number = bc_sip_lookup
transfer.sip_uri(agent_call_number, transfer_answer_webhook_url, transfer_answer_fallback_webhook_url,
transfer_disconnect_webhook_url)
else:
agent_call_number = format_phone_number(lead.my_phone)
transfer.phone_number(agent_call_number, transfer_answer_webhook_url, transfer_answer_fallback_webhook_url,
transfer_disconnect_webhook_url)
return create_xml_response(bxml)
except BandwidthException:
log.error(traceback.format_exc())
log.error('Error calling call id {}...'.format(lead.call_sid))
return ''
def mobile_outbound_transfer(lead_id, args):
"""
This function handles inbound transfers for mobile
"""
# Declare the bandwidth response xml tag. This will be used for the xml responses
bxml = Response()
# Get the to number that is being called by agent
c_to = args.get('to', '')
# Return the lead information
lead = Lead.query.filter(Lead.id == lead_id).first()
# This is the web-hook that will be called if a transfer is answered
transfer_answer_webhook_url = url_for(
'mobile_call_inbound.mobile_transfer_answer_bw_webhook', lead_id=lead.id,
_external=True, _scheme='https')
# This is the fallback web-hook that will be called if a transfer is answered
transfer_answer_fallback_webhook_url = url_for(
'mobile_call_inbound.mobile_transfer_answer_fallback_bw_webhook', lead_id=lead.id,
_external=True, _scheme='https')
# This is the web-hook that will be called if a transfer is disconnected
transfer_disconnect_webhook_url = url_for(
'mobile_call_inbound.mobile_transfer_disconnect_bw_webhook', lead_id=lead.id,
_external=True, _scheme='https')
# This is the web-hook that will be called if a transfer is completed
transfer_complete_webhook_url = url_for(
'mobile_call_inbound.mobile_transfer_complete_bw_webhook', inbound_id=lead.inbound_id, lead_id=lead.id,
_external=True, _scheme='https')
# This is the fallback web-hook that will be called if a transfer is completed
transfer_complete_fallback_webhook_url = url_for(
'mobile_call_inbound.mobile_transfer_complete_fallback_bw_webhook', inbound_id=lead.inbound_id, lead_id=lead.id,
_external=True, _scheme='https')
try:
caller_id = lead.my_phone
call_time_out = "65"
# Perform the transfer to the agent mobile sip
transfer = bxml.transfer(caller_id, call_time_out, transfer_complete_webhook_url,
transfer_complete_fallback_webhook_url, tag='transfer-initiated')
transfer.phone_number(c_to, transfer_answer_webhook_url, transfer_answer_fallback_webhook_url,
transfer_disconnect_webhook_url)
return create_xml_response(bxml)
except BandwidthException:
log.error(traceback.format_exc())
log.error('Error calling call id {}...'.format(lead.call_sid))
return ''
@mobile_call_inbound.route('/bw/mobile/voice/status', methods=['GET', 'POST'])
@csrf.exempt
def mobile_voice_status_callback(*args):
""" Entry point for the incoming voice status callback. This is specifically for Bandwidth API
V2. A Location exist with 2 application attach to it. One is for SMS/MMS and the other for Voice. This call-back url
is specified on the Voice application.
"""
from buyercall.lib.util_bandwidth import authenticate_bw_request
authenticated = authenticate_bw_request(request.headers)
if authenticated:
# Declare redis config url
redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])
# Fetch the request data. This includes message data for incoming sms/mms
args = request.json or request.args
# Fetch args values
call_id = args.get('callId', '')
event_type = args.get('eventType', '')
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', ''))
cause = args.get('cause', '')
tag = args.get('tag', '')
error_message = args.get('errorMessage', '')
# Look up lead in db based on the provider call sid
call_lead = Lead.query.filter(Lead.call_sid == call_id).first()
log.info('The status callback args: {}'.format(args))
try:
# Fetch storage from Redis based on call lead id
storage = CallStorage(redis_db, call_lead.id)
cause_description = storage.cause_description
if error_message:
storage.state = State.ERROR
storage.cause_description = error_message
log.info('Storage state is: {}'.format(storage.state))
log.info('The tag is: {}'.format(tag))
if storage.state not in ('ANSWERED', 'CAPTURED'):
if event_type == 'disconnect' and storage.state == 'NEW':
storage.state = 'MISSED'
cause_description = 'Call ended by caller before transfer can occurred.'
if storage.state == 'MISSED' and tag == 'transfer-complete':
cause_description = 'Call ended by callee.'
duration, response_time_seconds = 0, 0
if storage.connect_time:
connect_time = parser.parse(storage.connect_time)
response_time_seconds = (connect_time - start_time).total_seconds()
duration = (end_time - start_time).total_seconds()
agent = Agent.query.filter(Agent.id == storage.agent_id).first()
lead_mark_finished(call_lead.id, storage.state, response_time_seconds, duration, end_time,
cause, cause_description, tag, agent=agent)
else:
if tag == 'transfer-initiated':
call_lead.cause_description = 'Call ended by caller.'
db.session.commit()
elif tag == 'transfer-complete':
call_lead.cause_description = 'Call ended by callee.'
db.session.commit()
return ''
except Exception as e:
log.error('Unable to find call id; {} in the db. error: {}'.format(call_id, e))
return ''
else:
print('Authentication failed')
status_code = 401
message = jsonify(message='Authentication failed.')
response = make_response(message, status_code)
return response
@mobile_call_inbound.route('/bw/mobile/voice/status/fallback', methods=['GET', 'POST'])
@csrf.exempt
def voice_fallback_status_callback():
""" Entry point for the incoming voice fallback callback. This is specifically for Bandwidth API
V2. A Location exist with 2 application attach to it. One is for SMS/MMS and the other for Voice. This call-back url
is specified on the Voice application. This function will call the status callback endpoint.
"""
from buyercall.lib.util_bandwidth import authenticate_bw_request
authenticated = authenticate_bw_request(request.headers)
if authenticated:
# Fetch the request data. This includes message data for incoming sms/mms
args = request.json or request.args
return mobile_voice_status_callback(args)
else:
print('Authentication failed')
status_code = 401
message = jsonify(message='Authentication failed.')
response = make_response(message, status_code)
return response
@mobile_call_inbound.route('/api/bw/mobile-transfer-answer/webhook/<int:lead_id>', methods=['GET', 'POST'])
@csrf.exempt
def mobile_transfer_answer_bw_webhook(lead_id, *args):
""" This endpoint should be hit when a transfer is answered sending over an event. Additional bXML can be
invoked when this endpoint is hit. The only event to be sent in this
call back is: TransferAnswered
https://dev.bandwidth.com/voice/bxml/callbacks/transferAnswer.html
"""
# Declare redis config url
redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])
# Declare the redis db model
storage = CallStorage(redis_db, lead_id)
record_call = storage.routing_config.get('recordCalls', '')
transcribe_call = storage.routing_config.get('transcribeAnsweredCall', '')
# Get the params of the Request
args = request.json or request.args
log.info('The transfer answer callback response: {}'.format(args))
log.info('THE AGENT CALL ID IS: {}'.format(storage.agent_call_id))
transfer_call_id = args.get('callId')
connect_time = args.get('answerTime', '')
to_number = str(args.get('to', ''))
log.info('THE TO number is: {}'.format(to_number))
from .tasks import mobile_update_lead
mobile_update_lead.delay(lead_id, to_number=to_number, transfer_call_id=transfer_call_id,
connect_time=connect_time)
# Declare the bandwidth response xml tag. This will be used for the xml responses
bxml = Response()
# Set the answer tag
bxml.tag('transfer-answer')
if storage.sip_call_direction == 'inbound':
# Play a custom message to the agent before connecting with the lead
hidden_information = storage.routing_config.get('hiddenInformation', '')
if hidden_information:
bxml.say('.....')
bxml.say(hidden_information)
if record_call:
if storage.sip_call_direction == 'outbound':
bxml.say('...Please note that this call might be recorded.')
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')
if storage.sip_call_direction == 'inbound':
bxml.play_audio(app.config['BEEP_SOUND'])
return create_xml_response(bxml)
@mobile_call_inbound.route('/api/bw/mobile-transfer-answer-fallback/webhook/<int:lead_id>', methods=['GET', 'POST'])
@csrf.exempt
def mobile_transfer_answer_fallback_bw_webhook(lead_id):
""" This is the fallback callback url for the transfer answer callback event
https://dev.bandwidth.com/voice/bxml/callbacks/transferAnswer.html
"""
# Get the params of the Request
args = request.json or request.args
log.info('The transfer answer fallback callback response: {}'.format(args))
return mobile_transfer_answer_bw_webhook(lead_id, args)
@mobile_call_inbound.route('/api/bw/mobile-transfer-disconnect/webhook/<int:lead_id>', methods=['GET', 'POST'])
@csrf.exempt
def mobile_transfer_disconnect_bw_webhook(lead_id, *args):
""" This endpoint should be hit when a transfer is disconnected sending over an event. Additional bXML can not be
invoked when this endpoint is hit. The only event to be sent in this
call back is: TransferDisconnect
https://dev.bandwidth.com/voice/bxml/callbacks/transferDisconnect.html
"""
# Declare redis config url
redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=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
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', ''))
end_time = parser.parse(args.get('endTime', ''))
cause = args.get('cause', '')
error_message = args.get('errorMessage', '')
tag = args.get('tag', '')
# Lookup the Call information from the DB
call_lead = Lead.query.filter(Lead.id == lead_id).first()
duration, response_time_seconds, cause_description = None, None, None
if tag == 'transfer-answer' or tag == 'start-recording':
storage.state = State.CAPTURED
if storage.connect_time:
connect_time = parser.parse(storage.connect_time)
response_time_seconds = (connect_time - start_time).total_seconds()
duration = (end_time - start_time).total_seconds()
# Label call as complete
lead_mark_finished(call_lead.id, storage.state, response_time_seconds, duration, end_time,
cause, cause_description, tag)
# This following lines need to replace the else statement. This is a bug with BW not reporting the cancel
# cause properly. Once fix the cancel cause and error message should be included in the callback.
elif cause in ['cancel', 'rejected', 'timeout']:
storage.state = State.MISSED
storage.cause_description = error_message
else:
storage.state = State.MISSED
if not storage.cause_description:
storage.cause_description = 'Call ended by caller before it was answered by callee.'
if cause in ['error', 'node-capacity-exceeded', 'unknown', 'callback-error',
'invalid-bxml', 'application-error', 'account-limit']:
storage.state = State.ERROR
cause_description = error_message
# Label call as complete
lead_mark_finished(call_lead.id, storage.state, response_time_seconds, duration, end_time,
cause, cause_description, tag)
log.info('The transfer disconnect callback response: {}'.format(args))
return ''
@mobile_call_inbound.route('/api/bw/mobile-transfer-complete/webhook/<int:inbound_id>/<int:lead_id>', methods=['GET', 'POST'])
@csrf.exempt
def mobile_transfer_complete_bw_webhook(inbound_id, lead_id, *args):
""" This endpoint should be hit when a transfer is complete sending over an event. Additional bXML can be
invoked when this endpoint is hit after a transfer hangs-up. The only event to be sent in this
call back is: TransferComplete
https://dev.bandwidth.com/voice/bxml/callbacks/transferComplete.html
"""
# Declare redis config url
redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])
# Declare the redis db model
storage = CallStorage(redis_db, lead_id)
# BXML Response
bxml = Response()
# Get the params of the Request
args = request.json or request.args
log.info('The transfer complete callback response: {}'.format(args))
cause = args.get('cause', '')
error_message = args.get('errorMessage', '')
if cause in ['timeout', 'reject', 'cancel']:
storage.call_cause = cause
storage.cause_description = error_message
if storage.sip_call_direction == 'inbound':
return missed_call(storage, lead_id, inbound_id)
else:
bxml.tag('transfer-no-answer')
else:
bxml.tag('transfer-complete')
return create_xml_response(bxml)
@mobile_call_inbound.route('/api/bw/mobile-transfer-complete-fallback/webhook/<int:inbound_id>/<int:lead_id>', methods=['GET', 'POST'])
@csrf.exempt
def mobile_transfer_complete_fallback_bw_webhook(inbound_id, lead_id):
""" This is the fallback callback url for the transfer complete callback event
https://dev.bandwidth.com/voice/bxml/callbacks/transferComplete.html
"""
# Get the params of the Request
args = request.json or request.args
log.info('The transfer complete fallback callback response: {}'.format(args))
return mobile_transfer_complete_bw_webhook(lead_id, inbound_id, args)
@mobile_call_inbound.route('/mobile/inbound/call/<int:inbound_id>', methods=['GET', 'POST'])
@csrf.exempt
def mobile_inbound_call(inbound_id):
# This function is a call back url for SIP incoming phone calls.
# Whenever a call is made by a SIP account this call back url will be called.
# Retrieve mobile number configuration
mobile_pn = Phone.query.filter(Phone.id == inbound_id).first()
# import partnership information to get partnership id
from buyercall.blueprints.partnership.models import Partnership, PartnershipAccount
from buyercall.blueprints.mobile.utils import send_agent_push_notification
partner_account = PartnershipAccount.query \
.filter(PartnershipAccount.id == mobile_pn.partnership_account_id).first()
partner = Partnership.query.filter(Partnership.id == partner_account.partnership_id).first()
# Initiate Bandwidth API
client = bw_client(partner.id)
# Get the params of the Request
args = request.json or request.args
print(args)
# Determine some request data
event_type = args.get('eventType')
call_status = args.get('callState')
call_id = args.get('callId')
to_number = args.get('to')
from_number = args.get('from')
log.info('The event type is: {}'.format(event_type))
# Recording of calls setting
recording_enabled = mobile_pn.routing_config['recordCalls']
# Transcription settings for calls
try:
transcribe_call_enabled = mobile_pn.routing_config['transcribeAnsweredCall']
if transcribe_call_enabled:
transcribe_call_enabled_str = str(True)
else:
transcribe_call_enabled_str = str(False)
except Exception as e:
transcribe_call_enabled = False
transcribe_call_enabled_str = str(False)
try:
transcribe_voicemail_enabled = mobile_pn.routing_config['transcribeVoiceMail']
if transcribe_voicemail_enabled:
transcribe_voicemail_enabled_str = str(True)
else:
transcribe_voicemail_enabled_str = str(False)
except Exception as e:
transcribe_voicemail_enabled = False
transcribe_voicemail_enabled_str = str(False)
log.info('The transcribe call value is {}'.format(transcribe_call_enabled))
log.info('The transcribe voicemail value is {}'.format(transcribe_voicemail_enabled))
# Greeting whisper message to the inbound caller settings
greeting_enabled = mobile_pn.routing_config['greetingMessage']
greeting_type = mobile_pn.routing_config['whisperMessageType']
greeting_text = mobile_pn.routing_config['whisperMessage']
# Voicemail settings
voicemail_enabled = mobile_pn.routing_config['voicemail']
voicemail_type = mobile_pn.routing_config['voicemailMessageType']
voicemail_text = mobile_pn.routing_config['voicemailMessage']
# Get the sip account info
sip_endpoint = Endpoint.query.filter(Endpoint.inbound_id == inbound_id).first()
# This determines if the call is originated from a sip outbound call or lead inbound call
# Based off what the to number is. If the to number is a BuyerCall number we see it as incoming call
if sip_endpoint is not None:
if from_number == sip_endpoint.sip_uri:
lead_phone_number = to_number
# This determines if the call is originated from a sip outbound call or lead inbound call
sip_outbound = True
else:
lead_phone_number = from_number
# This determines if the call is originated from a sip outbound call or lead inbound call
sip_outbound = False
try:
# Listen for the incoming call event and create the lead and ping the webhook for call starting
if event_type == 'incomingcall':
log.info('The mobile callId is {} and the from number is {} and the to number is {}'
.format(call_id, from_number, to_number))
# Check to see if a lead exist already
lead = Lead.query.filter(Lead.call_sid == call_id).first()
# Lets set the lead or create it if it doesn't exist yet
if lead:
lead_id = lead.id
else:
from .tasks import mobile_lead_create
lead_new = mobile_lead_create(inbound_id, call_id, lead_phone_number, sip_outbound)
lead_id = lead_new
# Ping webhook to say a new call has started
webhooker.trigger_generic_webhook('mobile_start_call', lead_id)
# Listen for the answer event type. The BXML will only be used on answer event as per Bandwidth API doc
elif event_type == 'answer':
# Get the callback url for recording
record_callback = json.dumps('https://' + app.config.get('SERVER_DOMAIN') +
url_for('.mobile_inbound_call_record'))
log.info('The recording callback url is {}'.format(record_callback))
# Get the callback url for transcription
transcribe_callback = json.dumps('https://' + app.config.get('SERVER_DOMAIN') +
url_for('.mobile_inbound_call_transcribe'))
# Get the callback url for transfer
transfer_callback = json.dumps('https://' + app.config.get('SERVER_DOMAIN') +
url_for('.mobile_inbound_call_transfer', inbound_id=inbound_id))
# Set the call id used as tag in the transfer
call_id_tag = json.dumps(call_id)
# Check to see if it's outbound or inbound call and perform transfer
if sip_outbound:
# If it's an outbound call to the SIP then perform the below
outbound_transfer_to = json.dumps(lead_phone_number)
# Find the Bandwidth phone number associated with a sip endpoint and use it as Caller Id
bw_number = Phone.query.filter(Phone.id == inbound_id).first()
outbound_caller_id = json.dumps(bw_number.phonenumber)
if recording_enabled:
# create XML
outbound_xml_response = f'<Response><Transfer transferCallerId={outbound_caller_id}' \
f' transferTo={outbound_transfer_to} tag={call_id_tag}>' \
f'<Record requestUrl= {record_callback} fileFormat="wav" transcribe=' \
f'{json.dumps(transcribe_call_enabled_str)} transcribeCallbackUrl=' \
f'{transcribe_callback}/></Transfer></Response>'
log.info('The xml response looks like: {}'.format(outbound_xml_response))
return outbound_xml_response
else:
# create XML
outbound_xml_response = '<Response><Transfer transferCallerId={} transferTo={} tag= {}> ' \
'</Transfer></Response>'.\
format(outbound_caller_id, outbound_transfer_to, call_id_tag)
log.info('The xml response looks like: {}'.format(outbound_xml_response))
return outbound_xml_response
else:
is_number_blocked = Block.blocked(inbound_id, args.get('from'))
if is_number_blocked:
log.info('This call was not processed because phone number: {} was blocked'.format(
args.get('from')
))
blocked_hangup_xml = '<Response><Hangup></Hangup></Response>'
return blocked_hangup_xml
# Retrieve the agent that's associated with the endpoint
agent = Agent.query.filter(Agent.id == sip_endpoint.agent_id).first()
if agent.available_now is False:
log.info('Agent, name: {} {} and id: {} is not available according to their schedule'
.format(agent.firstname, agent.lastname, agent.id))
if voicemail_enabled:
unavail_vm_end_xml = '<Record requestUrl={} fileFormat="wav" transcribe={} ' \
'transcribeCallbackUrl={} tag="voicemail" />'\
.format(record_callback, json.dumps(transcribe_voicemail_enabled_str),
transcribe_callback)
if voicemail_type == 'text':
unavail_vm_intro_xml = ' <SpeakSentence voice="susan" locale="en_US" ' \
'gender="female" volume="4">{}</SpeakSentence>'\
.format(voicemail_text)
else:
vm_audio = Audio.query \
.filter(and_(Audio.whisper_message_type == 'voicemailMessage',
Audio.inbound_id == inbound_id)).first()
if vm_audio:
vm_audio_link = vm_audio.audio_url
else:
vm_audio_link = ''
unavail_vm_intro_xml = '<PlayAudio volume="4">{}</PlayAudio>'.format(vm_audio_link)
# Get the beep sound for voicemail
vm_beep_sound = app.config.get('BEEP_SOUND')
unavail_vm_beep_xml = '<PlayAudio volume="4">{}</PlayAudio>'.format(vm_beep_sound)
# Create the full VM BMXL
unavail_vm_xml = unavail_vm_intro_xml + unavail_vm_beep_xml + unavail_vm_end_xml
log.info('The vm for unavbailable xml looks like {}'.format(unavail_vm_xml))
return '<Response>{}</Response>'.format(unavail_vm_xml)
else:
unavailable_hangup_xml = '<Response><Hangup></Hangup></Response>'
return unavailable_hangup_xml
# Else if it's an inbound call to the SIP then perform the below
inbound_transfer_to = json.dumps(sip_endpoint.sip_uri)
# Set the XML to construct BXML for transfer
start_response_xml = '<Response>'
start_transfer_xml = '<Transfer transferTo=' + inbound_transfer_to + ' tag=' + call_id_tag +\
' callTimeout="23" requestUrl=' + transfer_callback + ' >'
end_transfer_xml = '</Transfer>'
end_response_xml = '</Response>'
if recording_enabled:
record_xml = '<Record requestUrl=' + \
record_callback + ' fileFormat="wav" transcribe=' + \
json.dumps(transcribe_call_enabled_str) + \
' transcribeCallbackUrl=' + transcribe_callback + ' />'
else:
record_xml = ''
if greeting_enabled:
if greeting_type == 'text':
greet_xml = '<SpeakSentence voice="susan" locale="en_US" gender="female" volume="4">' + \
greeting_text + '</SpeakSentence>'
else:
greet_audio = Audio.query \
.filter(and_(Audio.whisper_message_type == 'whisperMessage',
Audio.inbound_id == inbound_id)).first()
if greet_audio:
greet_audio = greet_audio.audio_url
else:
greet_audio = ''
greet_xml = '<PlayAudio volume="4">{}</PlayAudio>'.format(greet_audio)
else:
greet_xml = ''
if voicemail_enabled:
vm_end_xml = '<Record requestUrl=' + record_callback + \
' fileFormat="wav" transcribe=' + \
json.dumps(transcribe_voicemail_enabled_str) + ' transcribeCallbackUrl=' + \
transcribe_callback + ' tag="voicemail" />'
if voicemail_type == 'text':
vm_intro_xml = ' <SpeakSentence voice="susan" locale="en_US" gender="female" volume="4">' \
+ voicemail_text + '</SpeakSentence>'
else:
vm_audio = Audio.query \
.filter(and_(Audio.whisper_message_type == 'voicemailMessage',
Audio.inbound_id == inbound_id)).first()
if vm_audio:
vm_audio_link = vm_audio.audio_url
else:
vm_audio_link = ''
vm_intro_xml = '<PlayAudio volume="4">{}</PlayAudio>'.format(vm_audio_link)
# Get the beep sound for voicemail
vm_beep_sound = app.config.get('BEEP_SOUND')
vm_beep_xml = '<PlayAudio volume="4">{}</PlayAudio>'.format(vm_beep_sound)
# Create the full VM BMXL
vm_xml = vm_intro_xml + vm_beep_xml + vm_end_xml
else:
vm_xml = ''
# create XML
inbound_xml_response = "{}{}{}{}{}{}{}".format(start_response_xml, greet_xml,
start_transfer_xml, record_xml,
end_transfer_xml, vm_xml,
end_response_xml)
log.info('The xml response looks like: {}'.format(inbound_xml_response))
return inbound_xml_response
# Cancel the call if the event is hangup
if event_type == 'hangup':
final_call = client.calls.info(call_id)
# Check to see if a lead exist already
lead = Lead.query.filter(Lead.call_sid == call_id).first()
log.info('The call id is {}'.format(call_id))
call_duration = final_call.get('chargeableDuration')
if lead:
lead.duration = call_duration
db.session.commit()
# Retrieve the agent that's associated with the endpoint
agent = Agent.query.filter(Agent.id == sip_endpoint.agent_id).first()
if agent.available_now is False and voicemail_enabled is False:
if lead:
lead.status = 'missed'
db.session.commit()
webhooker.trigger_generic_webhook('mobile_end_call', lead.id)
contact = Contact.query.filter(Contact.id == lead.contact_id).first()
if contact and agent and contact.agent_id != agent.id:
contact.agent_id = agent.id
contact.agent_assigned = agent.full_name
db.session.commit()
send_agent_push_notification(contact)
elif agent.available_now is False and voicemail_enabled:
if lead:
lead.status = 'missed'
db.session.commit()
try:
is_num_blocked = Block.blocked(inbound_id, args.get('from'))
if is_num_blocked:
if lead:
lead.status = 'blocked'
db.session.commit()
webhooker.trigger_generic_webhook('mobile_end_call', lead.id)
contact = Contact.query.filter(Contact.id == lead.contact_id).first()
if contact and agent and contact.agent_id != agent.id:
contact.agent_id = agent.id
contact.agent_assigned = agent.full_name
db.session.commit()
send_agent_push_notification(contact)
except Exception as e:
log.info('The caller is not in the blocked number list')
# Check when the transfer is complete and get a new id
elif event_type == 'transferComplete':
original_call_id = args.get('tag')
# Update the lead with the transfer call id
# Check to see if a lead exist already
lead = Lead.query.filter(Lead.call_sid == original_call_id).first()
lead.call_sid = call_id
if args.get('callState') == 'completed':
final_call = client.calls.info(original_call_id)
# Get the sip account info
sip_endpoint = Endpoint.query.filter(Endpoint.inbound_id == lead.inbound_id).first()
log.info('The call id is {}'.format(call_id))
call_duration = final_call.get('chargeableDuration')
from .tasks import mobile_lead_update
mobile_lead_update(lead, sip_endpoint.agent_id, call_duration)
if recording_enabled is False:
webhooker.trigger_generic_webhook('mobile_end_call', lead.id)
agent = Agent.query.filter(Agent.id == sip_endpoint.agent_id).first()
contact = Contact.query.filter(Contact.id == lead.contact_id).first()
if contact and agent and contact.agent_id != agent.id:
contact.agent_id = agent.id
contact.agent_assigned = agent.full_name
db.session.commit()
send_agent_push_notification(contact)
db.session.commit()
except BandwidthException:
log.error(traceback.format_exc())
return ''
else:
log.info('No sip endpoint exist and associated with inbound id: {}'.format(inbound_id))
return ''
@mobile_call_inbound.route('/mobile/inbound/callRecording/', methods=['GET', 'POST'])
@csrf.exempt
def mobile_inbound_call_record():
# This function is a call back url for SIP incoming phone calls.
# Whenever a call is complete and record is enabled by a SIP account this call back url will be called.
# Get the params of the Request
args = request.json or request.args
log.info('The recording callback info is: {}'.format(args))
record_status = args.get('status')
call_id = args.get('callId')
recording_id = args.get('recordingId')
lead = Lead.query.filter(Lead.call_sid == call_id).first()
lead.recording_id = recording_id
db.session.commit()
sip_endpoint = Endpoint.query.filter(Endpoint.inbound_id == lead.inbound_id).first()
# Retrieve mobile number configuration
mobile_pn = Phone.query.filter(Phone.id == lead.inbound_id).first()
# Retrieve transcription setting. If on, the web-hook gets sent after transcription. If off it will be sent now.
call_transcribe_enabled = False
try:
if mobile_pn.routing_config['transcribeAnsweredCall']:
call_transcribe_enabled = mobile_pn.routing_config['transcribeAnsweredCall']
except Exception as e:
log.info('There is no transcribe answered call boolean set for this number id: {}'.format(mobile_pn.id))
vm_transcribe_enabled = False
try:
if mobile_pn.routing_config['transcribeVoiceMail']:
vm_transcribe_enabled = mobile_pn.routing_config['transcribeVoiceMail']
except Exception as e:
log.info('There is no transcribe voicemail boolean set for this number id: {}'.format(mobile_pn.id))
try:
if record_status == 'complete':
from buyercall.blueprints.phonenumbers.bw_tasks import bw_upload_recording
from buyercall.blueprints.mobile.utils import send_agent_push_notification
agent = Agent.query.filter(Agent.id == sip_endpoint.agent_id).first()
bw_upload_recording(call_id)
if lead.status == 'missed':
contact = Contact.query.filter(Contact.id == lead.contact_id).first()
if contact is not None and contact.user_fullname not in ('', ' '):
contact_detail = contact.user_fullname
else:
contact_detail = format_phone_number_bracket(lead.phonenumber)
vm_push_msg = 'New voicemail from ' + contact_detail
from buyercall.blueprints.mobile.tasks import push_notification
push_notification(sip_username=sip_endpoint.sip_username,
push_type='NotifyGenericTextMessage',
message=vm_push_msg)
if vm_transcribe_enabled is False:
webhooker.trigger_generic_webhook('mobile_end_call', lead.id)
contact = Contact.query.filter(Contact.id == lead.contact_id).first()
if contact and agent and contact.agent_id != agent.id:
contact.agent_id = agent.id
contact.agent_assigned = agent.full_name
db.session.commit()
send_agent_push_notification(contact)
elif lead.status == 'completed' and call_transcribe_enabled is False:
webhooker.trigger_generic_webhook('mobile_end_call', lead.id)
contact = Contact.query.filter(Contact.id == lead.contact_id).first()
if contact and agent and contact.agent_id != agent.id:
contact.agent_id = agent.id
contact.agent_assigned = agent.full_name
db.session.commit()
send_agent_push_notification(contact)
except BandwidthException:
log.error(traceback.format_exc())
return ''
@mobile_call_inbound.route('/mobile/inbound/callTranscribe/', methods=['GET', 'POST'])
@csrf.exempt
def mobile_inbound_call_transcribe():
# This function is a call back url for SIP incoming phone calls.
# Get the params of the Request
args = request.json or request.args
recording_id = args.get('recordingId')
lead = Lead.query.filter(Lead.recording_id == recording_id).first()
lead.transcription_text = args.get('text')
db.session.commit()
webhooker.trigger_generic_webhook('mobile_end_call', lead.id)
agent = Agent.query.filter(Agent.id == lead.agent_id).first()
contact = Contact.query.filter(Contact.id == lead.contact_id).first()
if contact and agent and contact.agent_id != agent.id:
contact.agent_id = agent.id
contact.agent_assigned = agent.full_name
db.session.commit()
from buyercall.blueprints.mobile.utils import send_agent_push_notification
send_agent_push_notification(contact)
log.info('The transcribe callback info is: {}'.format(args))
return ''
@mobile_call_inbound.route('/mobile/inbound/callTransfer/<int:inbound_id>', methods=['GET', 'POST'])
@csrf.exempt
def mobile_inbound_call_transfer(inbound_id):
# This function is a call back url for SIP incoming phone calls.
# Retrieve mobile number configuration
mobile_pn = Phone.query.filter(Phone.id == inbound_id).first()
# import partnership information to get partnership id
from buyercall.blueprints.partnership.models import Partnership, PartnershipAccount
partner_account = PartnershipAccount.query \
.filter(PartnershipAccount.id == mobile_pn.partnership_account_id).first()
partner = Partnership.query.filter(Partnership.id == partner_account.partnership_id).first()
# Initiate Bandwidth API
client = bw_client(partner.id)
voicemail_enabled = mobile_pn.routing_config['voicemail']
# Get the params of the Request
args = request.json or request.args
log.info('The transfer callback info is: {}'.format(args))
call_id = args.get('callId')
call_hangup_cause = args.get('cause')
original_call_id = args.get('tag')
if args.get('callState') == 'completed':
final_call = client.calls.info(original_call_id)
# Check to see if a lead exist already
lead = Lead.query.filter(Lead.call_sid == original_call_id).first()
# Get the sip account info
sip_endpoint = Endpoint.query.filter(Endpoint.inbound_id == lead.inbound_id).first()
log.info('The call id is {}'.format(call_id))
call_duration = final_call.get('chargeableDuration')
from .tasks import mobile_lead_update
mobile_lead_update(lead, sip_endpoint.agent_id, call_duration, call_hangup_cause)
if voicemail_enabled is False or call_hangup_cause in ['ORIGINATOR_CANCEL', 'USER_BUSY', 'CALL_REJECTED']:
webhooker.trigger_generic_webhook('mobile_end_call', lead.id)
agent = Agent.query.filter(Agent.id == lead.agent_id).first()
contact = Contact.query.filter(Contact.id == lead.contact_id).first()
if contact and agent and contact.agent_id != agent.id:
contact.agent_id = agent.id
contact.agent_assigned = agent.full_name
db.session.commit()
from buyercall.blueprints.mobile.utils import send_agent_push_notification
send_agent_push_notification(contact)
else:
log.info('The call; {} is active'.format(call_id))
return ''
def lead_mark_finished(lead_id, new_state, response_time_seconds=None, duration=None,
end_time=None, cause=None, cause_description=None, tag=None, agent=None):
# Declare redis config url
redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])
storage = CallStorage(redis_db, lead_id)
lead = Lead.query.options(load_only("id", "status")).filter(
Lead.id == lead_id
).first()
if storage.call_cause:
cause = storage.call_cause
if storage.cause_description:
cause_description = storage.cause_description
log.info('THE RESPONSE TIME: {}'.format(response_time_seconds))
if new_state == State.MISSED:
lead.status = 'missed'
lead.duration = 60
lead.endtime = end_time
lead.missed_call_cause = cause
lead.cause_description = cause_description
elif new_state == State.CAPTURED:
lead.status = 'completed'
lead.response_time_seconds = response_time_seconds
lead.duration = duration
lead.endtime = end_time
lead.missed_call_cause = 'completed'
if cause_description:
lead.cause_description = cause_description
elif new_state == State.BLOCKED:
lead.status = 'blocked'
lead.response_time_seconds = response_time_seconds
lead.duration = duration
lead.endtime = end_time
lead.missed_call_cause = cause
lead.cause_description = cause_description
elif new_state == State.ERROR:
lead.status = 'error'
lead.response_time_seconds = response_time_seconds
lead.duration = duration
lead.endtime = end_time
lead.missed_call_cause = cause
lead.cause_description = cause_description
lead.call_count += 1
db.session.commit()
# Check to see if recording is turned on. If so then we will send the web hook if the recording call back
recording = storage.routing_config.get('recordCalls', '')
voicemail = storage.routing_config.get('voicemail', '')
log.info('THE TAG IS: {}'.format(tag))
log.info('THE LEAD STATUS IS: {}'.format(lead.status))
from ..phonenumbers.bw_operational_tasks import delay_webhook_trigger
if lead.status == 'completed' and not recording:
print('a')
delay_webhook_trigger.apply_async(args=['mobile_end_call', lead_id], countdown=15)
elif lead.status == 'completed' and tag not in ['start-recording']:
print('ab')
delay_webhook_trigger.apply_async(args=['mobile_end_call', lead_id], countdown=15)
elif lead.status == 'missed' and not voicemail:
print('abc')
delay_webhook_trigger.apply_async(args=['mobile_end_call', lead_id], countdown=15)
elif lead.status == 'missed' and tag in ['pre-record', 'transfer-initiated', 'pre-transfer']:
print('abcd')
delay_webhook_trigger.apply_async(args=['mobile_end_call', lead_id], countdown=15)
elif lead.status == 'error':
print('abcde')
delay_webhook_trigger.apply_async(args=['mobile_end_call', lead_id], countdown=15)
elif lead.status == 'blocked':
print('abcdef')
delay_webhook_trigger.apply_async(args=['mobile_end_call', lead_id], countdown=15)
contact = Contact.query.filter(Contact.id == lead.contact_id).first()
if contact and agent and contact.agent_id != agent.id:
contact.agent_id = agent.id
contact.agent_assigned = agent.full_name
db.session.commit()
# Send push notifications after call
from buyercall.blueprints.mobile.utils import send_agent_push_notification
send_agent_push_notification(contact)
return ''
def missed_call(storage, lead_id, inbound_id):
# Declare the bandwidth response xml tag. This will be used for the xml responses
bxml = Response()
storage.state = State.MISSED
lead = Lead.query.join(Lead.inbound).filter(Lead.id == lead_id).first()
lead.status = 'missed'
db.session.commit()
if storage.routing_config.get('voicemail', ''):
if storage.routing_config.get('voicemailMessageType', '') == 'audio':
vm_audio = Audio.query \
.filter(and_(Audio.whisper_message_type == 'voicemailMessage',
Audio.inbound_id == inbound_id))\
.order_by(Audio.id.desc()).first()
if vm_audio:
vm_audio_link = vm_audio.audio_url
bxml.tag('pre-record')
bxml.play_audio(vm_audio_link)
bxml.custom_pause('1')
else:
vm_greet = storage.routing_config.get('voicemailMessage', '')
if storage.routing_config.get('voicemail') == 'es':
bxml.tag('pre-record')
bxml.custom_pause('1')
bxml.say(vm_greet, 'female', 'es_MX', 'esperanza')
bxml.custom_pause('1')
else:
bxml.tag('pre-record')
bxml.custom_pause('2')
bxml.say(vm_greet)
bxml.custom_pause('1')
record_call_back_url = url_for(
'bw_operational.operational_call_record_bw_webhook', lead_id=lead.id,
_external=True, _scheme='https')
if storage.routing_config.get('transcribeVoiceMail', ''):
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.play_audio(app.config['BEEP_SOUND'])
bxml.tag('post-record')
bxml.record(recording_available_url=record_call_back_url, transcribe=transcribe,
transcribe_available_url=transcribe_call_back_url, tag='voicemail')
else:
bxml.hangup()
return create_xml_response(bxml)