File: //home/arjun/projects/buyercall_forms/buyercall/buyercall/blueprints/widgets/routing.py
import logging
import random
import traceback
from datetime import datetime
import json
from flask import (
url_for, current_app as app,
request
)
from sqlalchemy import and_, or_
from twilio.base.exceptions import TwilioRestException
import redis
import pytz
import xml.etree.ElementTree as et
from buyercall.extensions import db
from buyercall.lib.flask_mailplus import _try_renderer_template
from buyercall.lib.util_lists import first, to_string
from buyercall.lib.util_twilio import (
BusyAgents,
CallStorage,
InboundCallState as State,
subaccount_client,
)
from buyercall.lib.util_webhooks import WebhookUtil
from ..agents.models import Agent
from ..user.models import User
from ..leads.models import Lead
from buyercall.blueprints.filters import format_phone_number
AGENT_TIMEOUT = 23
DAYS = 86400 # The length of a day in seconds
AGENT_WHISPER_HELLO = """Hello ... .."""
AGENT_WHISPER_LEAD_NAME = """The lead's name is: {} . {} ."""
AGENT_WHISPER_QUESTION = """The lead is asking: {}. """
log = logging.getLogger(__name__)
webhooker = WebhookUtil()
class FileFormatException(Exception):
pass
class NoAgentsException(Exception):
pass
def phonecall_inprogress(widget, phoneNumber='', **kwargs):
if widget and phoneNumber:
phone_number = normalize(phoneNumber)
lead_on_call = Lead.query\
.filter(
phone_number == Lead.phonenumber,
widget.partnership_account_id == Lead.partnership_account_id)\
.filter(or_(
Lead.STATUSES['in-progress'] == Lead.status,
Lead.STATUSES['ringing'] == Lead.status))\
.first()
if lead_on_call:
log.info('Cancelling widget {0} call request. Lead {1} with phonenumber {2} currently on call.'
.format(widget.id,
lead_on_call.id,
lead_on_call.phonenumber))
return True
return False
def add_widget_lead(
widget, firstName='', lastName='', emailAddress='', phoneNumber='',
sourceTag='', question='', status='ringing', **kwargs
):
if widget.inbound is None:
raise Exception('Widget {} has no inbound phone associated'.format(
widget.id
))
my_phone = widget.inbound.phonenumber
inbound_id = widget.inbound.id
sourceTag = widget.name
phone_number = normalize(phoneNumber)
if not firstName and not lastName:
firstName, lastName = Lead.get_last_known_name(
widget.partnership_account_id, phone_number
)
previous_lead = Lead.query.filter(
phone_number == Lead.phonenumber, widget.partnership_account_id == Lead.partnership_account_id
).first()
if previous_lead is not None:
progress_status = previous_lead.progress_status
agent_assigned = previous_lead.agent_assigned_id
else:
progress_status = 'no status'
agent_assigned = None
lead = Lead(
partnership_account_id=widget.partnership_account_id,
firstname=firstName,
lastname=lastName,
email=emailAddress,
phonenumber=phone_number,
progress_status=progress_status,
question=question,
starttime=datetime.utcnow(),
call_type='outbound',
status=status,
my_phone=my_phone,
widget_guid=widget.guid,
additional_info=kwargs,
call_source=sourceTag,
agent_assigned_id=agent_assigned,
originating_number=my_phone,
inbound_id=inbound_id,
)
db.session.add(lead)
db.session.commit()
# Add contact based on new lead using celery
from .tasks import add_contact
add_contact(phone_number, firstName, lastName, emailAddress, widget.partnership_account_id)
return lead
def normalize(phone_number):
""" Convert a US phone number into an E.123 format.
"""
# str.translate and unicode.translate work differently, so we make sure we
# have an str object.
stripped = phone_number = str(phone_number).strip()
if not phone_number.startswith('sip:'):
stripped = phone_number.translate(str.maketrans({' ': '', '.': '', '(': '', '-': '', ')': ''}))
if stripped.startswith('+'):
return stripped
if len(stripped) == 10: # US number w/o country code
return '+1' + stripped
if len(stripped) == 11 and stripped.startswith('1'): # Missing '+'
return '+' + stripped
return stripped
def get_available_agent_ids(widget):
"""Return a list of available agent IDs for a widgets."""
agent_ids = [
asg.agent_id
for asg in sorted(widget.assignments, key=lambda x: x.order)
if asg.agent.available_now
]
free_agents = BusyAgents.filter_from(agent_ids)
return free_agents
class Routing(object):
def __init__(self, client):
self.client = client
def save_lead_agent(self, lead_id, agent_id):
""" Save the agent on call to the database for this lead.
Return True if this is the agent chosen for the call, or False
if some other agent picked up first.
"""
# Declare redis url
redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])
storage = CallStorage(redis_db, lead_id)
called_agents_key = 'LIST{}'.format(lead_id)
with storage.lock():
if not storage.agent_id:
storage.agent_id = agent_id
else:
# Someone else picked up; exit
return False
# Cancel all other current calls
calls = redis_db.lrange(called_agents_key, 0, -1)
for call in calls:
a, sid = call.split('_')
if int(a) != agent_id:
log.info('Canceling call for agent {}...'.format(a))
self.client.calls.hangup(sid)
storage.agent_call_sid = request.args['callId']
lead = Lead.query.filter(Lead.id == lead_id).first()
lead.agent_id = agent_id
db.session.commit()
return True
def call_lead(self, widget, lead):
# Declare redis url
redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])
storage = CallStorage(redis_db, lead.id)
subscription = widget.partnership_account.subscription
if not subscription:
subscription = widget.partnership_account.partnership.subscription
storage.subaccount_sid = subscription.twilio_subaccount_sid
storage.clear_callback_cnt()
storage.state = State.NEW
webhooker.trigger_generic_webhook('operational_start_call', lead.id)
# Route the call
agent_ids = get_available_agent_ids(widget)
if not agent_ids:
log.warning('No agents available for widget {}.'.format(widget.id))
lead.status = 'missed'
db.session.commit()
send_notify_email(lead, widget)
raise NoAgentsException(lead)
if widget.options['routeSimultaneously']:
self.schedule_call_parallel(lead)
return lead
if widget.options['routeRandomly']:
random.shuffle(agent_ids)
self.route_sequential(agent_ids, widget, lead)
return lead
def schedule_call_parallel(self, lead):
from .tasks import call_parallel
call_parallel.delay(lead.id)
def call_agent_number(
self, agent, widget, lead_id, url=None, status_callback=None,
status_events=None, timeout=None, from_=None, if_machine=None, **kwargs
):
""" Call either the agent's phone number, mobile, or extension
"""
# Declare redis url
redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])
lead = Lead.query.filter(Lead.id == lead_id).first()
if not lead:
log.warning('Lead not found in the database')
return
# import partnership information to get partnership id
from ..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()
storage = CallStorage(redis_db, lead_id)
twilio_client = subaccount_client(storage.subaccount_sid, partner.id)
send_digits = None
wa = first(widget.assignments, lambda x: x.agent_id == agent.id)
if not wa:
log.error(
'Cannot find agent {} in widget {}\'s list of agents'.format(
agent.full_name, widget.id
))
return
if wa.contact_using == 'phone':
to = agent.phonenumber
elif wa.contact_using == 'mobile':
to = agent.mobile
elif wa.contact_using == 'extension':
to = agent.phonenumber
send_digits = 'wwww' + agent.extension if agent.extension else None
else:
raise Exception(
'Unknown calling method {} for agent {}, widget {}'.format(
repr(wa.contact_using), agent.full_name, widget.id
))
return twilio_client.calls.create(
to=to,
from_=from_,
url=url,
status_callback=status_callback,
status_callback_event=status_events,
send_digits=send_digits,
timeout=timeout,
)
def route_sequential(self, agent_ids, widget, lead):
# Declare redis url
redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])
# Remove any agents that joined a call in the meantime
agent_ids = BusyAgents.filter_from(agent_ids)
agent = None
while agent_ids:
agent = Agent.query.filter(and_(
Agent.partnership_account_id == lead.partnership_account_id,
Agent.id == agent_ids[0]
)).first()
if agent:
break
agent_ids = agent_ids[1:]
log.info('The lead call status is: {}'.format(lead.status))
if not agent_ids or agent is None or lead.status == 'completed':
log.error('No more agents available for widget {}.'.format(
widget.id
))
redis_db.setex('CONNECT{}'.format(lead.id), 2 * DAYS, '-1')
if lead.status != 'completed':
lead.status = 'missed'
db.session.commit()
send_notify_email(lead, widget)
schedule_retries(lead)
return
# Reset the agent who answered
redis_db.setex('CALL{}'.format(lead.id), 4 * DAYS, '0')
try:
log.debug('Trying call from {} to agent {}...'.format(
widget.name, agent.id
))
self.call_agent_sequential(
lead, agent, widget, agent_ids)
db.session.commit()
except TwilioRestException:
log.error('Error caling agent {}; retrying...'.format(agent_ids[0]))
self.route_sequential(agent_ids[1:], widget, lead)
def call_agent_sequential(self, lead, agent, widget, agent_ids):
return self.call_agent_number(
agent,
widget,
url=url_for(
'twilio_api.agent_digit_prompt',
lead_id=lead.id,
agent_id=agent.id,
_external=True,
_scheme='https'
),
status_callback=url_for(
'twilio_api.agent_sequential_call_status',
widget_guid=widget.guid,
lead_id=lead.id,
agent_id=agent.id,
other_agents=to_string(agent_ids[1:]),
_external=True,
_scheme='https'
),
status_callback_event=['ringing', 'answered', 'completed'],
timeout=AGENT_TIMEOUT,
from_=widget.inbound.phonenumber,
lead_id=lead.id,
)
def call_agent_parallel(self, lead, agent, widget):
return self.call_agent_number(
agent,
widget,
url=url_for(
'twilio_api.agent_digit_prompt',
lead_id=lead.id,
agent_id=agent.id,
_external=True,
_scheme='https'
),
status_callback=url_for(
'twilio_api.agent_parallel_call_status',
agent_id=agent.id,
lead_id=lead.id,
_external=True,
_scheme='https'
),
status_callback_event=['ringing', 'answered', 'completed'],
timeout=AGENT_TIMEOUT,
# from_=widget.inbound.phonenumber,
# FIXME:
from_=widget.inbound.phonenumber,
lead_id=lead.id,
)
class BandwidthRouting(Routing):
def __init__(self, client):
self.client = client
"""
call_lead: Entry point for widget calls. The api/call endpoint gets hit when a widget call request is made
through the widget gui that triggers this call_lead which have authenticated with Bandwidth.
"""
def call_lead(self, widget, lead):
# Declare redis url
redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])
# Initiate the redis storage used to store call details for handling call
storage = CallStorage(redis_db, lead.id)
# confirm the account's subscription. If not on account level then on partner level.
subscription = widget.partnership_account.subscription
if not subscription:
subscription = widget.partnership_account.partnership.subscription
storage.subaccount_sid = subscription.twilio_subaccount_sid
storage.clear_callback_cnt()
storage.state = State.NEW
# Fire off webhook for start of call
webhooker.trigger_generic_webhook('operational_start_call', lead.id)
# Get agents to whom the call needs to forward to
agent_ids = get_available_agent_ids(widget)
if not agent_ids:
log.warning('No agents available for widget {}.'.format(widget.id))
lead.status = 'missed'
db.session.commit()
send_notify_email(lead, widget)
raise NoAgentsException(lead)
# If the widget routing is set to simultaneous then use this function
if widget.options['routeSimultaneously']:
self.schedule_call_parallel(lead)
return lead
# Shuffle agents if random call routing is set for the widget
if widget.options['routeRandomly']:
random.shuffle(agent_ids)
self.route_sequential(agent_ids, widget, lead)
return lead
def schedule_call_parallel(self, lead):
from .bw_tasks import call_parallel
call_parallel.delay(lead.id)
def call_agent_number(
self, agent, widget, lead_id, url=None, status_callback=None,
status_events=None, timeout=None, from_=None, if_machine=None, **kwargs
):
""" Call either the agent's phone number, mobile, or extension
"""
answer_callback_url = kwargs.get('answer_callback_url', '')
answer_fallback_callback_url = kwargs.get('answer_fallback_callback_url', '')
disconnect_callback_url = kwargs.get('disconnect_callback_url', '')
wa = first(widget.assignments, lambda x: x.agent_id == agent.id)
if not wa:
log.error(
'Cannot find agent {} ({}) in widget {}\'s list of agents'.format(
agent.full_name, agent.id, widget.id
))
raise ValueError()
if wa.contact_using == 'phone':
to = agent.phonenumber
elif wa.contact_using == 'mobile':
to = agent.mobile
elif wa.contact_using == 'extension':
to = agent.phonenumber
# FIXME: Implement dialing an extension
# digits = 'wwww' + agent.extension if agent.extension else None
else:
raise Exception(
'Unknown calling method {} for agent {}, widget {}'.format(
repr(wa.contact_using), agent.full_name, widget.id
))
# 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)
# 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'
)
agent_call = self.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-widget-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))
json_call_string = json.loads(agent_call)
agent_call_id = json_call_string.get('callId', '')
return agent_call_id
def route_sequential(self, agent_ids, widget, lead):
# Declare redis url
redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'],
port=app.config['REDIS_CONFIG_PORT'])
# Remove any agents that joined a call in the meantime
agent_ids = BusyAgents.filter_from(agent_ids)
agent = None
while agent_ids:
agent = Agent.query.filter(and_(
Agent.partnership_account_id == lead.partnership_account_id,
Agent.id == agent_ids[0]
)).first()
if agent:
break
agent_ids = agent_ids[1:]
log.info('The lead call status is: {}'.format(lead.status))
if not agent_ids or agent is None or lead.status == 'completed':
log.error('No more agents available for widget {}.'.format(
widget.id
))
redis_db.setex('CONNECT{}'.format(lead.id), 2 * DAYS, '-1')
if lead.status != 'completed':
lead.status = 'missed'
db.session.commit()
send_notify_email(lead, widget)
schedule_retries(lead)
return
# Reset the agent who answered
redis_db.setex('CALL{}'.format(lead.id), 4 * DAYS, '0')
try:
log.debug('Trying call from {} to agent {}...'.format(
widget.name, agent.id
))
self.call_agent_sequential(
lead, agent, widget, agent_ids)
db.session.commit()
except Exception as e:
log.error('Error calling agent {}; Error: {}; retrying...'.format(agent_ids[0], e))
self.route_sequential(agent_ids[1:], widget, lead)
return ''
def call_agent_sequential(self, lead, agent, widget, agent_ids):
return self.call_agent_number(
agent,
widget,
answer_callback_url=url_for(
'bw_outbound.widget_outbound_agent_answer_webhook',
lead_id=lead.id,
agent_id=agent.id,
from_tn=widget.inbound.phonenumber,
_external=True,
_scheme='https'
),
answer_fallback_callback_url=url_for(
'bw_outbound.widget_outbound_agent_answer_fallback_webhook',
lead_id=lead.id,
agent_id=agent.id,
from_tn=widget.inbound.phonenumber,
_external=True,
_scheme='https'
),
disconnect_callback_url=url_for(
'bw_outbound.widget_sequence_outbound_agent_disconnect_webhook',
lead_id=lead.id,
agent_id=agent.id,
from_tn=widget.inbound.phonenumber,
other_agents=to_string(agent_ids[1:]),
_external=True,
_scheme='https'
),
timeout=AGENT_TIMEOUT,
from_=widget.inbound.phonenumber,
lead_id=lead.id,
)
def call_agent_parallel(self, lead, agent, widget):
return self.call_agent_number(
agent,
widget,
answer_callback_url=url_for(
'bw_outbound.widget_outbound_agent_answer_webhook',
lead_id=lead.id,
agent_id=agent.id,
from_tn=widget.inbound.phonenumber,
_external=True,
_scheme='https'
),
answer_fallback_callback_url=url_for(
'bw_outbound.widget_outbound_agent_answer_fallback_webhook',
lead_id=lead.id,
agent_id=agent.id,
from_tn=widget.inbound.phonenumber,
_external=True,
_scheme='https'
),
disconnect_callback_url=url_for(
'bw_outbound.widget_parallel_outbound_agent_disconnect_webhook',
lead_id=lead.id,
agent_id=agent.id,
from_tn=widget.inbound.phonenumber,
_external=True,
_scheme='https'
),
timeout=AGENT_TIMEOUT,
from_=widget.inbound.phonenumber,
lead_id=lead.id,
)
def get_agent_text(widget, lead):
""" Builds the text that the call center agent will head before picking up a
call. Agent Text = Custom Lead Message + Fullname + Question + Hidden Information + .
"""
# Declare redis url
redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])
agent_text = AGENT_WHISPER_HELLO
custom_lead_message = redis_db.get('CUSTOM_LEAD_MESSAGE:' + str(lead.id))
if lead.full_name:
agent_text += AGENT_WHISPER_LEAD_NAME.format(
lead.firstname, lead.lastname
)
if lead.question:
agent_text += AGENT_WHISPER_QUESTION.format(lead.question)
if widget.options.get('addHiddenInformation', False):
agent_text += widget.options.get('hiddenInformation', '').strip()
if agent_text and not agent_text.endswith('.'):
agent_text += '. '
if custom_lead_message is not None and custom_lead_message != "" and lead.call_source is not None and lead.call_source.lower() == 'adf':
agent_text = agent_text + ' . . . ' + custom_lead_message
return agent_text
def to_status(call_status):
""" Translate a Twilio call status into a Buyercall lead status.
"""
if call_status in ['busy', 'failed', 'canceled']:
return 'missed'
if call_status in ['no-answer']:
return 'unanswered'
if call_status in ['initiated', 'queued']:
return 'ringing'
if call_status in ['in-progress', 'completed']:
return call_status
return 'other'
def after_call_events(lead, widget):
from buyercall.lib.util_ses_email import send_ses_email
# send notification emails
send_notify_email(lead, widget)
# send CRM email
if widget.is_adf is True and str(lead.call_source).lower() == 'adf':
comment_text = "Call status: " + lead.status
agent_name = lead.agent_name
adf_emails = widget.adf_notification_emails
lead_source = lead.source
widget_phonenumber = widget.inbound.phonenumber
from buyercall.blueprints.partnership.models import PartnershipAccount
partner_account = PartnershipAccount.query\
.filter(PartnershipAccount.id == widget.partnership_account_id).first()
if agent_name is not None and agent_name is not '':
comment_text += ", Agent name: " + agent_name
if lead_source is not None and lead_source is not '':
comment_text += ", Call source: " + lead_source
if widget_phonenumber is not None and widget_phonenumber is not '':
comment_text += ", Phonenumber: " + widget_phonenumber
if widget.options.get('addHiddenInformation', False) and lead.status is 'completed':
hidden_info = widget.options.get('hiddenInformation', '').strip()
if hidden_info is not None and hidden_info is not '' and hidden_info is not ' ':
comment_text += ", Additional Information: " + hidden_info
email_body = update_adf_comment(lead.id, comment_text)
if len(email_body) > 0:
log.error("Sending CRM redirect email.")
if adf_emails is not None and len(adf_emails) > 0:
send_ses_email(recipients=adf_emails,
p_id=partner_account.partnership_id,
subject='ADF lead notification',
text=email_body
)
def send_notify_email(lead, widget):
from buyercall.lib.util_ses_email import send_ses_email
# Declare redis url
redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])
try:
if widget.options['notifyNone']:
return
if widget.options['notifyMissedLeads'] and lead.status != 'missed':
return
user = User.query.filter(
User.partnership_account_id == lead.partnership_account_id,
User.active,
User.is_deactivated.is_(False),
User.role == 'admin'
).first()
emails = widget.emails
app.logger.debug('The email list are {}'.format(emails))
from ..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()
ctx = vars(lead)
eastern = pytz.timezone('US/Eastern')
ctx['created_on'] = lead.created_on.astimezone(eastern).strftime('%c')
ctx['updated_on'] = lead.updated_on.astimezone(eastern).strftime('%c')
ctx['adf_updated_on'] = lead.interaction_time
ctx['company'] = partner.name
ctx['partner_logo'] = partner.logo
if lead.firstname:
ctx['caller_name_label'] = 'Caller Name:'
ctx['caller_name'] = ''.join((lead.firstname, ' ', lead.lastname))
if lead.widget:
ctx['reference'] = lead.widget.options.get('hiddenInformation', '')
if lead.source:
ctx['call_source'] = lead.source
if lead.status == 'missed':
# Render html template for email
missed_lead_email_template = _try_renderer_template('widgets/mail/missed_lead', ext='html', **ctx)
send_ses_email(recipients=emails,
p_id=partner.id,
subject=partner.name + ' - Missed lead notification',
html=missed_lead_email_template
)
else:
# Render html template for email
captured_lead_email_template = _try_renderer_template('widgets/mail/captured_lead', ext='html', **ctx)
send_ses_email(recipients=emails,
p_id=partner.id,
subject=partner.name + ' - Answered lead notification',
html=captured_lead_email_template
)
if widget.is_adf is True and str(lead.call_source).lower() != 'adf':
agent_name = lead.agent_name
adf_emails = widget.adf_notification_emails
phonenumber = widget.inbound.phonenumber
ctx['lead_comments'] = 'Call status: ' + lead.status
ctx['vehicle_year'] = ''
ctx['vehicle_make'] = ''
ctx['vehicle_model'] = ''
dealership = partner_account.name
if dealership:
ctx['vendor_name'] = dealership
else:
ctx['vendor_name'] = ''
call_lead_id = lead.id
if call_lead_id:
ctx['call_lead_id'] = call_lead_id
else:
ctx['call_lead_id'] = ''
custom_lead_vehicle_year = redis_db.get('CUSTOM_LEAD_YEAR:' + str(lead.id))
if custom_lead_vehicle_year is not None:
ctx['vehicle_year'] = custom_lead_vehicle_year
custom_lead_vehicle_make = redis_db.get('CUSTOM_LEAD_MAKE:' + str(lead.id))
if custom_lead_vehicle_make is not None:
ctx['vehicle_make'] = custom_lead_vehicle_make
custom_lead_vehicle_model = redis_db.get('CUSTOM_LEAD_MODEL:' + str(lead.id))
if custom_lead_vehicle_model is not None:
ctx['vehicle_model'] = custom_lead_vehicle_model
if agent_name is not None and agent_name is not '' and len(agent_name) > 2:
ctx['lead_comments'] = ctx['lead_comments'] + ', Agent name: ' + agent_name
if lead.source is not None and lead.source is not '':
ctx['receiver'] = lead.source
ctx['lead_comments'] = ctx['lead_comments'] + ', Call source: ' + lead.source
if phonenumber is not None and phonenumber is not '':
ctx['lead_comments'] = ctx['lead_comments'] + ', Phonenumber: ' + phonenumber
ctx['campaign_name'] = ''
ctx['campaign_exists'] = False
from buyercall.blueprints.contacts.models import Contact, Campaigns
contact = Contact.query.filter(
Contact.id == lead.contact_id,
Contact.partnership_account_id == lead.partnership_account_id)\
.first()
if contact and contact.campaign_id:
campaign = Campaigns.query.filter(
Campaigns.id == contact.campaign_id)\
.first()
if campaign:
ctx['campaign_exists'] = True
ctx['campaign_name'] = campaign.display_name
if adf_emails and len(adf_emails) > 0:
# Render text template for adf email
adf_lead_email_template = _try_renderer_template('widgets/mail/adf_lead', ext='txt', **ctx)
send_ses_email(recipients=adf_emails,
p_id=partner.id,
subject=partner.name + ' - ADF lead notification',
text=adf_lead_email_template
)
except Exception as e:
log.error(traceback.format_exc())
def schedule_retries(lead):
# Declare redis url
redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])
if lead.status in ['ringing', 'completed']:
# Nothing to do
log.debug('Cannot schedule retries yet/lead status is ' + lead.status)
return
storage = CallStorage(redis_db, lead.id)
cnt = storage.callback_cnt
routing_delays = lead.widget.options['retryRouting']
if len(routing_delays) <= cnt or not routing_delays[cnt]:
return False
delay = int(routing_delays[cnt])
log.info('Scheduling retry call for lead {} in {} minutes'.format(
lead.id, delay
))
lead.status = 'retry-pending'
db.session.commit()
# Get the service provider from inbound id to know which tasks to call
from ..phonenumbers.models import Phone
number = Phone.query.filter(Phone.id == lead.inbound_id).first()
try:
if number.provider == 'bandwidth':
from .bw_tasks import call_again
call_again.apply_async(args=[lead.id], countdown=(delay * 60))
else:
# This is for twilio call-backs
from .tasks import call_again
call_again.apply_async(args=[lead.id], countdown=(delay * 60))
return True
except Exception as e:
log.info('Unable to perform call-back or retry for lead: {} Error: {}'.format(lead.id, e))
return False
def update_adf_comment(lead_id, comment):
# Declare redis url
redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])
custom_lead_xml = redis_db.get('CUSTOM_LEAD_XML:' + str(lead_id))
if custom_lead_xml != None and len(custom_lead_xml) > 0:
try:
xmlentity = et.fromstring(custom_lead_xml)
prospect = xmlentity.find('prospect')
if prospect != None or prospect != "":
if prospect.find('customer') is not None:
customer = prospect.find('customer')
if customer.find('comments') is not None:
customer_comments = customer.find('comments').text
if len(customer_comments) > 0:
customer.find('comments').text = customer_comments + " - " + comment
else:
customer.find('comments').text = customer_comments + comment
else:
new_comment = et.SubElement(customer, 'comments')
new_comment.text = comment
else:
log.error("No customer found in the XML lead")
return et.tostring(xmlentity, encoding='utf8', method='xml')
else:
log.error("No prospect found in the XML lead")
except FileFormatException:
log.error("Error parsing XML file. Exception: "+sys.exc_info())
return ""