File: //home/arjun/projects/buyercall/buyercall/blueprints/phonenumbers/bw_operational_tasks.py
import logging as log
import traceback
import os
import pytz
from datetime import datetime
import redis
from flask import current_app, url_for
from buyercall.app import create_celery_app
from buyercall.lib.util_twilio import (
CallStorage,
InboundCallState as State,
split_name,
)
from buyercall.lib.util_webhooks import WebhookUtil
from ..filters import format_phone_number
from buyercall.extensions import db
from buyercall.lib.flask_mailplus import _try_renderer_template
from buyercall.lib.util_ses_email import send_ses_email
from ..leads.models import Lead
from ..phonenumbers.models import (
Phone,
HoldMusic,
Audio,
)
from tempfile import TemporaryFile
import uuid
import boto3
from sqlalchemy.orm import load_only
from ..contacts.models import Contact
from ..agents.models import Agent
from ..mobile.models import Endpoint
from ..partnership.models import PartnershipAccount, Partnership
HOURS = 3600
DAYS = 86400 # The length of a day in seconds
AGENT_TIMEOUT = 30
# TODO Parse Redis URL
celery = create_celery_app(current_app)
webhooker = WebhookUtil()
@celery.task
def bw_operational_lead_create(inbound_id, call_id, phone_number, caller_name=None, start_time=None, **kwargs):
# Declare redis url
redis_db = redis.StrictRedis(host=celery.conf['REDIS_CONFIG_URL'], port=celery.conf['REDIS_CONFIG_PORT'])
inbound = Phone.query.filter(Phone.id == inbound_id).first()
partner_account = PartnershipAccount.query \
.filter(PartnershipAccount.id == inbound.partnership_account_id).first()
from buyercall.lib.util_bandwidth import bw_client
client = bw_client(partner_account.partnership_id)
first_name, last_name = split_name(caller_name)
if (not first_name and not last_name) or (first_name == 'Unknown'):
first_name, last_name = Lead.get_last_known_name(
inbound.partnership_account_id, phone_number
)
previous_lead = Lead.query.filter(
phone_number == Lead.phonenumber,
inbound.partnership_account_id == Lead.partnership_account_id
).order_by(Lead.created_on.desc()).first()
if previous_lead is not None:
contact_id = previous_lead.contact_id
progress_status = previous_lead.progress_status
caller_display_id = previous_lead.caller_id
else:
contact_id = None
progress_status = 'no status'
# Return Bandwidth call for CNAM value for lead
if celery.conf.get('DEBUG', False):
info = client.cnam_lookup(phone_number, test=True)
else:
info = client.cnam_lookup(phone_number)
if not info:
caller_display_id = ''
else:
caller_display_id = info
lead = Lead(
partnership_account_id=inbound.partnership_account_id,
firstname=first_name,
lastname=last_name,
phonenumber=phone_number,
email='',
starttime=datetime.utcnow(),
call_sid=call_id,
caller_id=caller_display_id,
call_type='inbound',
my_phone=inbound.phonenumber,
call_source=inbound.friendly_name,
inbound_id=inbound_id,
status='ringing',
contact_id=contact_id,
originating_number=phone_number,
progress_status=progress_status
)
db.session.add(lead)
db.session.commit()
# Query the leads table to retrieve the latest lead that was added
latest_lead = Lead.query\
.filter(Lead.phonenumber == phone_number)\
.order_by(Lead.created_on.desc())\
.first()
# Query contacts to see if lead already exist in the contacts table
contact = Contact.query\
.filter(Contact.phonenumber_1 == latest_lead.phonenumber)\
.filter(Contact.partnership_account_id == inbound.partnership_account_id)\
.first()
if contact:
contact.updated_on = latest_lead.updated_on
contact.caller_id = caller_display_id
db.session.commit()
else:
contact = Contact(
firstname=first_name,
lastname=last_name,
caller_id=caller_display_id,
phonenumber_1=phone_number,
email='',
partnership_account_id=inbound.partnership_account_id,
)
db.session.add(contact)
db.session.commit()
new_contact = Contact.query\
.filter(Contact.phonenumber_1 == latest_lead.phonenumber)\
.filter(Contact.partnership_account_id == inbound.partnership_account_id)\
.first()
if new_contact:
latest_lead.contact_id = new_contact.id
db.session.commit()
else:
log.info('no lead contact exist')
# Associate the lead ID with the Bandwidth call ID
key = 'CALL:{}'.format(call_id)
redis_db.setex(key, 2*HOURS, lead.id)
storage = CallStorage(redis_db, lead.id)
storage.init()
storage.call_sid = call_id
storage.routing_config = inbound.routing_config
storage.clear_retry_cnt()
storage.clear_callback_cnt()
storage.caller_number = phone_number
storage.start_time = start_time
# TODO: Bandwidth user ID
storage.subaccount_sid = None
storage.state = State.NEW
storage.inbound = inbound.phonenumber
if previous_lead is not None:
storage.caller_name = caller_name
return lead.id
@celery.task
def bw_operational_update_lead(lead_id, **kwargs):
# Declare redis url
redis_db = redis.StrictRedis(host=celery.conf['REDIS_CONFIG_URL'], port=celery.conf['REDIS_CONFIG_PORT'])
storage = CallStorage(redis_db, lead_id)
# Declare kwargs parameters
to_number = kwargs.get('to_number', '')
log.info('THE to number IS: {}'.format(to_number))
transfer_call_id = kwargs.get('transfer_call_id', '')
connect_time = kwargs.get('connect_time', '')
# Get lead data from db
lead = Lead.query.filter(Lead.id == lead_id).first()
if lead:
# Get the agent id that answered the phone call
answer_agent_id = ''
if to_number.startswith('sip'):
answer_agent_id = Endpoint.sip_agent_id(to_number)
else:
log.info('The first storage agent list is: {}'.format(storage.agent_list))
agent_list = storage.agent_list
log.info('The storage agent list is: {}'.format(agent_list))
for agent in agent_list:
answer_agent_id = agent.get("agent_id", 0)
log.info('THE AGENT ID IS: {}'.format(answer_agent_id))
lead.agent_id = int(answer_agent_id)
lead.transfer_call_id = transfer_call_id
lead.status = 'in-progress'
storage.state = State.ANSWERED
storage.connect_time = connect_time
db.session.commit()
# Send a webhook because the call has been answered and we know which agent picked up
webhooker.trigger_generic_webhook('operational_agent_call', lead_id)
if answer_agent_id and lead:
contact = Contact.query.filter(Contact.id == lead.contact_id).first()
agent = Agent.query.filter(Agent.id == answer_agent_id).first()
if contact and agent and contact.agent_id != agent.id:
contact.agent_id = answer_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.error('No lead was found for lead id: {}'.format(lead_id))
return ''
@celery.task
def retry_once_more(lead_id, **kwargs):
""" Try calling the agents once more, up to the maximum number of retries.
Return False if the maximum number of retries exceeded, otherwise True.
"""
# Declare redis url
redis_db = redis.StrictRedis(host=celery.conf['REDIS_CONFIG_URL'], port=celery.conf['REDIS_CONFIG_PORT'])
storage = CallStorage(redis_db, lead_id)
routing_config = storage.routing_config
routing = ''
if routing_config.get('routingType', '') == 'digits':
digit_routing = routing_config['digitRoutings']
for route in digit_routing:
if route['dialDigit'] == storage.pressed_digit:
routing = route
else:
routing = routing_config['defaultRouting']
log.info('The routing looks like: {}'.format(routing))
cnt = storage.inc_retry_cnt()
log.info('The retry count is: {}'.format(cnt))
if cnt <= int(routing['retryRouting']):
log.debug('Retrying once more for lead {}...'.format(lead_id))
return True
return False
@celery.task
def bw_upload_recording(lead_id, **kwargs):
""" After the call, grab the first recording from Bandwidth, if any, and
upload to S3.
"""
lead = Lead.query.filter(Lead.id == lead_id).first()
# Check to see if transcription is turned on. If so then we will send the web hook if the transcription call back
inbound = Phone.query.filter(Phone.id == lead.inbound_id).first()
partner_account = PartnershipAccount.query\
.options(load_only('name', 'partnership_id')).filter(PartnershipAccount.id == lead.partnership_account_id).first()
from buyercall.lib.util_bandwidth import bw_client
if inbound.type == 'mobile':
client = bw_client(partner_account.partnership_id, request_type='voice', tn_type='mobile')
else:
client = bw_client(partner_account.partnership_id, request_type='voice')
# Get the media url from the request
recording_id = kwargs.get('recording_id', '')
call_id = kwargs.get('call_id', '')
get_recording = client.call.recording(call_id, recording_id)
file_path = os.path.join(celery.conf['UPLOAD_FOLDER'], f"recording-{recording_id}")
with open(file_path, 'wb') as f:
count = 0
for c in get_recording.iter_content(chunk_size=1024):
count += len(c)
f.write(c)
log.info("Read {} bytes.".format(count))
hex_value = uuid.uuid4().hex
key = '{}_{}/{}_{}_{}'.format(
partner_account.name, lead.partnership_account_id, lead.id, 'recording', hex_value)
content_type = 'audio/mpeg'
bucket = celery.conf['RECORDING_BUCKET']
from buyercall.lib.util_boto3_s3 import upload_file
upload_file(file_path, bucket, key, content_type)
os.remove(file_path)
# Update recording link
updated_lead = Lead.query.options(
load_only("id", "recording_url")
).filter(Lead.id == lead.id).first()
updated_lead.recording_url = '{}::{}'.format(celery.conf['RECORDING_BUCKET'], key)
db.session.commit()
call_transcribe = inbound.routing_config.get('transcribeAnsweredCall', '')
vm_transcribe = inbound.routing_config.get('transcribeVoiceMail', '')
if lead.status == 'missed' and not vm_transcribe:
if inbound.type == 'mobile':
webhooker.trigger_generic_webhook('mobile_end_call', lead_id)
else:
webhooker.trigger_generic_webhook('operational_end_call', lead_id)
if lead.status == 'completed' and not call_transcribe:
if inbound.type == 'mobile':
webhooker.trigger_generic_webhook('mobile_end_call', lead_id)
else:
webhooker.trigger_generic_webhook('operational_end_call', lead_id)
@celery.task
def delay_webhook_trigger(event, lead_id, **kwargs):
webhooker.trigger_generic_webhook(event, lead_id)
return ''
@celery.task
def send_notifications(lead_id, **kwargs):
# Declare redis config url
redis_db = redis.StrictRedis(host=celery.conf['REDIS_CONFIG_URL'], port=celery.conf['REDIS_CONFIG_PORT'])
from ..widgets.models import split_emails
lead = Lead.query.filter(Lead.id == lead_id).first()
storage = CallStorage(redis_db, lead.id)
# if storage.manual_call:
# log.debug('Notification email suppressed.')
# return
inbound = Phone.query.filter(
Phone.id == lead.inbound_id,
Phone.partnership_account_id == lead.partnership_account_id
).first()
if not inbound:
return
notifications = inbound.notifications
routings = inbound.routing_config
if notifications.get('notifyLeads', 'none') == 'none':
return
# Check to see if a sms needs to be sent to the lead and send sms if turned on
if lead.status == 'missed':
if routings.get('configSMSSetup') and routings.get('MissedCallAutoReply'):
from ..sms.views import send_text_message
text = routings.get('MissedCallAutoReplyText') + ' Reply STOP to unsubscribe.'
media_url = ''
send_text_message(lead.inbound_id, lead.phonenumber, text, media_url)
else:
log.info('The SMS Configuration is turned off for this phone number')
# Send eventual notifications
emails = []
adf_emails = []
user_email = None
from ..user.models import User
admin = User.query.filter(
User.role == 'admin',
User.partnership_account_id == lead.partnership_account_id
).first()
if admin:
user_email = admin.email
if inbound.routing_config.get('routingType', '') == 'digits':
agent_ids = []
for digit in inbound.routing_config['digitRoutings']:
for a in digit['agents']:
agent_ids.append(a['id'])
else:
agent_ids = [
a['id'] for a in inbound.routing_config['defaultRouting']['agents']
]
from ..agents.models import Agent
agent_emails = [
a.email
for a in Agent.query.options(load_only('email')).filter(
Agent.partnership_account_id == lead.partnership_account_id,
Agent.id.in_(agent_ids)
).all()
]
if (notifications.get('notifyLeads', 'none') == 'missed'
and lead.status == 'missed'):
emails.extend(split_emails(notifications.get('notifyMissedCustom', '')))
if notifications.get('notifyMissedAgents'):
emails.extend(agent_emails)
if notifications.get('notifyMissedMe') and user_email:
emails.append(user_email)
if notifications.get('notifyLeads', 'none') == 'all':
emails.extend(split_emails(notifications.get('notifyAllCustom', '')))
if notifications.get('notifyAllAgents'):
emails.extend(agent_emails)
if notifications.get('notifyAllMe') and user_email:
emails.append(user_email)
if not emails:
log.warning('Nowhere to send {} lead notification!'.format(
lead.status
))
return
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
if lead.firstname:
ctx['caller_name_label'] = 'Caller Name:'
ctx['caller_name'] = ''.join((lead.firstname, ' ', lead.lastname))
if lead.caller_id:
ctx['caller_id_label'] = 'Caller Id:'
ctx['caller_id'] = lead.caller_id
if lead.recording_url:
ctx['vm_div_style'] = 'block'
else:
ctx['vm_div_style'] = 'none'
if lead.source:
ctx['call_source'] = lead.source
if lead.inbound:
ctx['reference'] = lead.inbound.routing_config.get(
'hiddenInformation', ''
)
ctx['partner_logo'] = partner.logo
ctx['company'] = partner.name
if partner.partner_url:
ctx['lead_contact_url'] = partner.partner_url + '/contacts/contact/' + str(lead.contact_id)
else:
ctx['lead_contact_url'] = url_for('contacts.contact_lead_page', id=lead.contact_id, _external=True,
_scheme='https')
try:
log.debug('The email list are {}'.format(emails))
if routings['notifyAdf'] is not None and routings['notifyAdf'] is 'True' or routings['notifyAdf']:
if routings['notifyAdfCustom'] is not None and routings['notifyAdfCustom'] is not '':
multi_adf_emails = split_emails(routings['notifyAdfCustom'])
if multi_adf_emails is not None:
adf_emails.extend(multi_adf_emails)
else:
adf_emails.append(routings['notifyAdfCustom'])
ctx['lead_comments'] = 'Call status: ' + lead.status
ctx['receiver'] = ''
agent_name = lead.agent_name
friendly_name = lead.source
phonenumber = lead.inbound.phonenumber
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'] = ''
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 friendly_name is not None and friendly_name is not '':
ctx['lead_comments'] = ctx['lead_comments'] + ', Call source: ' + friendly_name
ctx['receiver'] = friendly_name
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 is not None and len(adf_emails) > 0:
# Render text template for adf email
adf_lead_email_template = _try_renderer_template('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
)
if lead.status == 'missed' and storage.state == State.MISSED:
# Render html template for email
missed_lead_email_template = _try_renderer_template('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
)
elif lead.status == 'completed' and storage.state == State.CAPTURED:
# Render html template for email
completed_lead_email_template = _try_renderer_template('mail/captured_lead', ext='html', **ctx)
send_ses_email(recipients=emails,
p_id=partner.id,
subject=partner.name + ' - Answered lead notification',
html=completed_lead_email_template
)
else:
# Render html template for email
captured_lead_email_template = _try_renderer_template('mail/captured_lead', ext='html', **ctx)
send_ses_email(recipients=emails,
p_id=partner.id,
subject=partner.name + ' - New lead notification',
html=captured_lead_email_template
)
except:
log.error(traceback.format_exc())