HEX
Server: Apache/2.4.52 (Ubuntu)
System: Linux spn-python 5.15.0-89-generic #99-Ubuntu SMP Mon Oct 30 20:42:41 UTC 2023 x86_64
User: arjun (1000)
PHP: 8.1.2-1ubuntu2.20
Disabled: NONE
Upload Files
File: //home/arjun/projects/buyercall_forms/buyercall/buyercall/blueprints/widgets/rest_api_twilio.py
import logging as log
import traceback
from datetime import datetime

from flask import (
    Blueprint,
    current_app as app,
    request,
    url_for,
)
from flask_cors import cross_origin
from twilio.twiml.voice_response import VoiceResponse
import redis

from buyercall.extensions import db, csrf
from buyercall.lib.util_lists import to_ints
from buyercall.lib.util_twilio import (
    CallStorage, subaccount_client, update_response_time,
    BusyAgents, InboundCallState as State
)

from .routing import (
    get_agent_text,
    schedule_retries,
    Routing,
    after_call_events,
)
from buyercall.blueprints.leads.models import Lead
from buyercall.blueprints.agents.models import Agent


DEFAULT_VOICE = 'alice'

DAYS = 86400  # The length of a day in seconds

twilio_api = Blueprint('twilio_api', __name__, template_folder='templates')


@twilio_api.route('/api/twiml/agent1/<int:lead_id>', methods=['POST'])
@csrf.exempt
def agent_digit_prompt(lead_id):
    """ An agent is connected. Whisper the lead information to the agent, and
    dial the lead.
    """
    agent_id = request.args['agent_id']

    log.debug('Agent {} picked up'.format(agent_id))

    r = VoiceResponse()

    # How do we greet the agent?
    lead = Lead.query.join(Lead.widget).filter(Lead.id == lead_id).first()
    agent_text = get_agent_text(lead.widget, lead)

    if agent_text:
        r.say(agent_text, voice=DEFAULT_VOICE)

    # If digit prompt is disabled for the phone number, just continue
    if not lead.widget.inbound.routing_config.get('digitPrompt', False):
        r.redirect(url_for(
            'twilio_api.twiml_agent_digit_press_result',
            agent_id=agent_id,
            lead_id=lead_id,
            _external=True,
            _scheme='https'
        ))
        return str(r)

    with r.gather(
        action=url_for(
            'twilio_api.twiml_agent_digit_press_result',
            agent_id=agent_id,
            lead_id=lead_id,
            _external=True,
            _scheme='https'
        ),
        numDigits=1,
        timeout=int(app.config.get('AMD_DIGIT_PROMPT_TIMEOUT', 5)),
        finishOnKey=''
    ) as g:
        g.say(
            'Press any key to accept this call, or this call with be canceled.',
            voice=DEFAULT_VOICE
        )

    r.say('This call will be canceled now.', voice=DEFAULT_VOICE)

    return str(r)


@twilio_api.route(
    '/api/twiml/agent2/<int:agent_id>/<int:lead_id>', methods=['POST'])
@csrf.exempt
def twiml_agent_digit_press_result(agent_id, lead_id):
    # Declare redis url
    redis_db = redis.StrictRedis(
        host=app.config['REDIS_CONFIG_URL'],
        port=app.config['REDIS_CONFIG_PORT'],
        decode_responses=True
    )

    from .tasks import call_lead

    digits = request.form.get('Digits', '')
    log.debug('The agent pressed {}'.format(digits))

    storage = CallStorage(redis_db, lead_id)
    storage.state = State.AGENT_ONHOLD

    lead = Lead.query.join(Lead.widget).filter(Lead.id == lead_id).first()
    # import partnership information to get partnership id
    from buyercall.blueprints.partnership.models import Partnership, PartnershipAccount
    partner_account = PartnershipAccount.query \
        .filter(PartnershipAccount.id == lead.partnership_account_id).first()
    # Get the partner id to get relevant twilio credentails with twilio client
    partner = Partnership.query.filter(Partnership.id == partner_account.partnership_id).first()

    client = subaccount_client(storage.subaccount_sid, partner.id)

    r = VoiceResponse()

    called_agents_key = 'LIST{}'.format(lead_id)

    with storage.lock():
        if not storage.agent_id:
            storage.agent_id = agent_id
        calls = redis_db.lrange(called_agents_key, 0, -1)

    if storage.agent_id != agent_id:
        return str(r)  # Someone else picked up; exit

    # Cancel all other current calls
    for call in calls:
        a, sid = call.split('_')
        if int(a) != agent_id:
            log.info('Canceling call for agent {}...'.format(a))
            client.calls(sid).update(status='completed')

    lead.agent_id = agent_id
    db.session.commit()

    with r.dial() as d:
        d.conference(
            name='lead_%d' % lead_id,
            muted=False,
            beep=False,
            startConferenceOnEnter=True,
            endConferenceOnExit=True,
            waitUrl=url_for(
                'twilio_inbound.twiml_hold_music',
                file_guid='loop.mp3',
                _external=True,
                _scheme='https'
            ),
            waitMethod='GET'
        )

    call_lead.delay(lead_id, agent_id)

    return str(r)


@twilio_api.route(
    '/api/twiml/lead2/<int:lead_id>/<int:agent_id>', methods=['POST']
)
@csrf.exempt
def lead_greeting(lead_id, agent_id):
    """ Greet the lead.
    """
    lead = Lead.query.join(Lead.widget).filter(Lead.id == lead_id).first()
    lang = lead.widget.options.get('language', 'en')
    lead_text = lead.widget.options.get('whisperMessage', '')

    r = VoiceResponse()
    if lead_text:
        r.say(lead_text, language=lang, voice=DEFAULT_VOICE)

    r.redirect(url_for(
        'twilio_api.lead_on_connect',
        lead_id=lead_id,
        agent_id=agent_id,
        _external=True,
        _scheme='https'
    ))

    return str(r)


@twilio_api.route(
    '/api/twiml/lead3/<int:lead_id>/<int:agent_id>', methods=['POST']
)
@csrf.exempt
def lead_on_connect(lead_id, agent_id):
    """ An agent and the lead are both talking. Mark call as in-progress.
    """
    # Declare redis url
    redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])

    redis_db.setex('CONNECT{}'.format(lead_id), 2 * DAYS, '1')
    redis_db.setex('SUCC{}'.format(lead_id), 2 * DAYS, '1')

    lead = Lead.query.filter(Lead.id == lead_id).first()
    lead.agent_id = agent_id
    lead.status = 'in-progress'
    db.session.commit()

    r = VoiceResponse()
    with r.dial() as d:
        d.conference(
            name='lead_%d' % lead_id,
            muted=False,
            beep=True,
            startConferenceOnEnter=True,
            endConferenceOnExit=True,
            waitUrl='',
            waitMethod='GET',
            record=lead.widget.options['recordCalls'],
            statusCallback=url_for(
                'twilio_api.lead_conference_callback',
                lead_id=lead_id,
                _external=True,
                _scheme='https'
            )
        )

    return str(r)


@twilio_api.route(
    '/api/lead_conference_callback/<int:lead_id>', methods=['POST'])
@csrf.exempt
@cross_origin()
def lead_conference_callback(lead_id):
    recording_url = request.form.get('RecordingUrl', '')
    if not recording_url:
        return
    lead = Lead.query.filter(Lead.id == lead_id).first()
    lead.recording_url = recording_url
    db.session.commit()
    return ''


@twilio_api.route(
    '/api/lead_outbound_call_status/<int:lead_id>', methods=['POST'])
@csrf.exempt
@cross_origin()
def lead_outbound_call_status(lead_id):
    """ Handle status of an attempt to initiate a call with a lead.
    """
    # Declare redis url
    redis_db = redis.StrictRedis(
        host=app.config['REDIS_CONFIG_URL'],
        port=app.config['REDIS_CONFIG_PORT'],
        decode_responses=True
    )

    # The agent who's currently being called
    call_status = request.form.get('CallStatus', '')
    success = redis_db.get('SUCC{}'.format(lead_id))
    r = VoiceResponse()

    if call_status == 'completed' and success == '1':
        lead = Lead.query.filter(Lead.id == lead_id).first()
        lead.status = 'completed'

        if lead.call_source == 'form':
            vehicle_details = redis_db.get('CUSTOM_LEAD_MESSAGE: {}'.format(lead.id))
            agent = Agent.query.filter(Agent.id == lead.agent_id).one()
            if agent is not None:
                lead.question += '{} - Agent: {} {}'.format(vehicle_details, agent.firstname, agent.lastname)
            else:
                lead.question += vehicle_details

        db.session.add(lead)
        db.session.commit()

        try:
            after_call_events(lead, lead.widget)
            # send_notify_email(lead, lead.widget)
        except Exception as e:
            log.error(traceback.format_exc())
    elif call_status in ['queued', 'ringing', 'in-progress']:
        pass
    elif call_status in ['busy', 'no-answer']:
        r.say('The lead is not answering our call.', voice=DEFAULT_VOICE)
        lead = Lead.query.filter(Lead.id == lead_id).first()
        lead.status = 'unanswered'
        db.session.add(lead)
        db.session.commit()
    elif call_status in ['failed', 'canceled']:
        pass

    return str(r)


@twilio_api.route(
    '/api/agent_parallel_call_status/<int:agent_id>/<int:lead_id>',
    methods=['POST']
)
@csrf.exempt
@cross_origin()
def agent_parallel_call_status(agent_id, lead_id):
    """ Handle status updates when initiating calls with agents in parallel.
    """
    # Declare redis url
    redis_db = redis.StrictRedis(
        host=app.config['REDIS_CONFIG_URL'],
        port=app.config['REDIS_CONFIG_PORT']
    )

    # The agent who's currently being called
    call_status = request.form.get('CallStatus', '')
    answered_by = request.form.get('AnsweredBy', '')

    if call_status == 'ringing':
        update_response_time(call_status, agent_id, lead_id, redis_db)
        return ''
    if call_status == 'in-progress':
        BusyAgents.add(agent_id)
        update_response_time(call_status, agent_id, lead_id, redis_db)
        return ''
    if call_status == 'canceled':
        # Someone else picked up
        return ''
    if call_status in ['initiated', 'queued']:
        # The future is uncertain...
        return ''

    # The call has ended
    BusyAgents.remove(agent_id)

    if answered_by == 'machine':
        call_status = 'no-answer'

    log.info('Call update for agent {}: status is {}'.format(
        agent_id, call_status
    ))

    called_agents_key = 'LIST{}'.format(lead_id)

    ongoing_calls = 1

    storage = CallStorage(redis_db, lead_id)

    with storage.lock():
        calls = redis_db.lrange(called_agents_key, 0, -1)
        for call in calls:
            a, sid = call.decode().split('_')
            if int(a) == agent_id:
                log.info('Removing call for agent {} from list...'.format(a))
                redis_db.lrem(called_agents_key, 1, call)
                break
        ongoing_calls = redis_db.llen(called_agents_key)
        log.info('Still {} calls ongoing...'.format(ongoing_calls))

    if ongoing_calls > 0:
        return ''

    call_duration = int(request.form.get('CallDuration', '0'))
    from buyercall.blueprints.partnership.models import PartnershipAccount
    lead = Lead.query.join(Lead.partnership_account).join(
        PartnershipAccount.partnership
    ).filter(Lead.id == lead_id).first()
    agent_update_lead(lead, agent_id, call_duration)
    return '', 200


@twilio_api.route(
    '/api/agent_sequential_call_status/<int:lead_id>', methods=['POST'])
@csrf.exempt
@cross_origin()
def agent_sequential_call_status(lead_id):
    """ Handle status updates when initiating calls agents in sequence.
    """
    # Declare redis url
    redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])

    # The agent who's currently being called
    from buyercall.blueprints.partnership.models import PartnershipAccount, Partnership
    agent_id = int(request.args['agent_id'])
    call_status = request.form.get('CallStatus', '')

    lead = Lead.query.join(Lead.partnership_account).join(
        PartnershipAccount.partnership
    ).filter(Lead.id == lead_id).first()

    partner_account = PartnershipAccount.query \
        .filter(PartnershipAccount.id == lead.partnership_account_id).first()
    # Get the partner id to get relevant twilio credentails with twilio client
    partner = Partnership.query.filter(Partnership.id == partner_account.partnership_id).first()

    answered_by = request.form.get('AnsweredBy', '')

    if call_status in ['ringing', 'in-progress']:
        update_response_time(call_status, agent_id, lead_id, redis_db)

    if answered_by == 'machine':
        call_status = 'no-answer'

    storage = CallStorage(redis_db, lead_id)
    client = subaccount_client(storage.subaccount_sid, partner.id)
    r = Routing(client)

    if call_status == 'queued' or call_status == 'ringing':
        lead.status = call_status
        db.session.commit()
        return ''
    elif call_status == 'in-progress':
        # Save the agent id to the leads table
        BusyAgents.add(agent_id)

        lead.status = call_status
        lead.agent_id = agent_id
        lead.call_sid = request.form.get('CallSid', '')
        db.session.commit()
        return ''
    elif call_status == 'completed':
        BusyAgents.remove(agent_id)

        # Save the agent id to the leads table
        call_duration = int(request.form.get('CallDuration', '0'))
        agent_update_lead(lead, agent_id, call_duration)

        if redis_db.get('SUCC{}'.format(lead_id)) != '1':
            agent_ids = to_ints(request.args['other_agents'])
            r.route_sequential(agent_ids, lead.widget, lead)

        return ''
    else:  # call_status in ['busy', 'failed', 'no-answer', 'canceled']:
        BusyAgents.remove(agent_id)

        agent_ids = to_ints(request.args['other_agents'])
        r.route_sequential(agent_ids, lead.widget, lead)
        return ''


@twilio_api.route('/api/record_voicemail/<int:lead_id>', methods=['POST'])
@csrf.exempt
@cross_origin()
def record_voicemail(lead_id):
    recording_url = request.form.get('RecordingUrl', '')
    lead = Lead.query.filter(Lead.id == lead_id).first()
    lead.recording_url = recording_url
    db.session.add(lead)
    db.session.commit()
    return '', 200


def agent_update_lead(lead, agent_id, call_duration):
    """ Update the lead information at the end of a call.
    """
    # Declare redis url
    redis_db = redis.StrictRedis(
        host=app.config['REDIS_CONFIG_URL'],
        port=app.config['REDIS_CONFIG_PORT'],
        decode_responses=True
    )

    success = (redis_db.get('SUCC{}'.format(lead.id)) == '1')
    log.info('success is {}'.format(success))
    if not success:
        redis_db.setex('CONNECT{}'.format(lead.id), 2 * DAYS, '-1')

    if success:
        lead.status = 'completed'
    elif lead.status == 'ringing':
        lead.status = 'missed'
    lead.agent_id = agent_id if success else None
    lead.call_sid = request.form.get('CallSid', '')
    lead.call_count += 1
    lead.endtime = datetime.utcnow()
    lead.duration = call_duration

    if call_duration:
        subscription = lead.partnership_account.subscription
        if not subscription:
            subscription = lead.partnership_account.partnership.subscription
        subscription.update_usage(lead.partnership_account_id, seconds=call_duration)
    db.session.commit()

    if not success:
        try:
            after_call_events(lead, lead.widget)
            # send_notify_email(lead, lead.widget)
        except Exception as e:
            log.error(traceback.format_exc())
        schedule_retries(lead)