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.py
import itertools
from uuid import uuid4
import logging as log
import traceback
import sys
import redis
import xml.etree.ElementTree as et

from flask import (
    request, jsonify, make_response, Blueprint, current_app
)
from flask_cors import cross_origin
from flask_login import current_user, login_required
from sqlalchemy import and_, or_
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import load_only
from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy.sql import text
from sqlalchemy.sql.expression import func, case
from buyercall.extensions import db, csrf
from buyercall.lib.util_twilio import subaccount_client
from buyercall.lib.util_bandwidth import bw_client
from buyercall.lib.util_rest import rest_method, rest_partnership_account
from .models import Widget, AgentAssignment, User
from .routing import (
    BandwidthRouting,
    Routing,
    add_widget_lead,
    phonecall_inprogress,
    NoAgentsException,
    after_call_events,
)
from buyercall.blueprints.billing.decorators import subscription_required
from buyercall.blueprints.phonenumbers.models import Phone
from buyercall.blueprints.leads.models import Lead
from buyercall.blueprints.agents.models import Agent
from buyercall.blueprints.billing.models.subscription import Subscription


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

DAYS = 86400  # The length of a day in seconds


def json_error(message, error_code=500):
    """ Build an Ajax response with the given message and code. """
    return make_response(jsonify(error=message), error_code)


@widgets_api.route('/api/outbound', methods=['GET'])
@login_required
def get_widgets():
    """
    Retrieves all widgets for the current user.
    """
    search = request.args.get('search[value]', '')
    order = int(request.args.get('order[0][column]', '-1'))
    direction = request.args.get('order[0][dir]', 'asc')
    offset = int(request.args.get('start', 0))
    limit = int(request.args.get('length', 99))
    if limit == -1:
        limit = 99

    # Check if being viewed by super partner
    partnership_account_id = current_user.partnership_account_id

    if current_user.is_viewing_partnership:
        partnership_account_id = current_user.get_user_viewing_partnership_account_id

    columns = [
        'id', 'name', 'lead_count', 'created_on', 'updated_on', 'enabled'
    ]

    total = Widget.query.options(
        load_only('id', 'name', 'created_on', 'updated_on', 'enabled')
    ).filter(
        Widget.partnership_account_id == partnership_account_id
    )

    leads = Lead.query.filter(
        Lead.partnership_account_id == partnership_account_id
    ).with_entities(
        Lead.widget_guid,
        func.sum(1).label('lead_count')
    ).group_by(Lead.widget_guid).subquery()

    filtered = total
    if search:
        pattern = '%{}%'.format(search)
        filtered = total.filter(Widget.name.ilike(pattern))

    filtered = filtered.outerjoin(
        (leads, Widget.guid == leads.c.widget_guid)
    ).with_entities(
        Widget.id,
        Widget.name,
        case(
            [(leads.c.lead_count.is_(None), 0)],
            else_=leads.c.lead_count
        ).label('lead_count'),
        Widget.created_on,
        Widget.updated_on,
        Widget.enabled
    )

    sorted_ = filtered
    if order in range(len(columns)):
        order_pred = '{} {}'.format(columns[order], direction)
        sorted_ = sorted_.order_by(text(order_pred))
    sorted_ = sorted_.offset(offset).limit(limit)

    data = [
        {i: row[i] for i in range(len(row))} for row in sorted_.all()
    ]
    subscription = current_user.subscription
    for row in data:
        if subscription:
            row[6] = bool(subscription.usage_over_limit)
        else:
            row[6] = False
    return jsonify(
        draw=request.args['draw'],
        recordsFiltered=filtered.count(),
        recordsTotal=total.count(),
        data=data
    )


@widgets_api.route('/api/outbound', methods=['POST'])
@login_required
def add_widget():
    """
    Creates a new widget.
    Returns the ID of the newly created database row, as JSON.
    """
    # If we're saving the widget for the first time, and the name already
    # exists, change it.

    guid = str(uuid4())
    options = request.get_json()
    options['name'] = fix_name(options['name'])
    options['guid'] = guid

    # Check if being viewed by super partner
    partnership_account_id = current_user.partnership_account_id

    if current_user.is_viewing_partnership:
        partnership_account_id = current_user.get_user_viewing_partnership_account_id

    user_object = User.query.filter(
        User.partnership_account_id == partnership_account_id
    ).first()

    company_before_format = user_object.company.replace(" ", "")
    company_after_format = ''.join(e for e in company_before_format if e.isalnum())

    widget_count = Widget.query.filter(
        Widget.partnership_account_id == partnership_account_id
    ).count() + 1

    if widget_count > 0:
        widget_count_string = str(widget_count)
    else:
        widget_count_string = "0{}".format(widget_count)
    widget = Widget(
        email="{}{}@inbound.buyercall.com".format(company_after_format, widget_count_string),
        partnership_account_id=partnership_account_id,
        guid=guid,
        name=options['name'],
        options=options
    )
    db.session.add(widget)
    phone = Phone.query.filter(
        Phone.id == options['fromNumberId'], Phone.partnership_account_id == partnership_account_id
    ).first()
    if phone:
        widget.inbound = phone

    db.session.commit()
    return jsonify(id=widget.id, name=widget.name)


@widgets_api.route('/api/outbound/disable/<int:id_>', methods=['POST'])
@login_required
def disable_widget(id_):
    # Check if being viewed by super partner
    partnership_account_id = current_user.partnership_account_id

    if current_user.is_viewing_partnership:
        partnership_account_id = current_user.get_user_viewing_partnership_account_id

    widget = Widget.query.options(
        load_only('id', 'enabled')
    ).filter(Widget.id == id_, Widget.partnership_account_id == partnership_account_id).first()
    widget.enabled = False
    db.session.commit()
    return ''


@widgets_api.route('/api/outbound/enable/<int:id_>', methods=['POST'])
@login_required
def enable_widget(id_):
    # Check if being viewed by super partner
    partnership_account_id = current_user.partnership_account_id

    if current_user.is_viewing_partnership:
        partnership_account_id = current_user.get_user_viewing_partnership_account_id

    widget = Widget.query.options(
        load_only('id', 'enabled')
    ).filter(Widget.id == id_, Widget.partnership_account_id == partnership_account_id).first()
    widget.enabled = True
    db.session.commit()
    return ''


@widgets_api.route('/api/outbound/delete/<int:id_>', methods=['POST', 'DELETE'])
@login_required
def delete_widget(id_):
    # Check if being viewed by super partner
    partnership_account_id = current_user.partnership_account_id

    if current_user.is_viewing_partnership:
        partnership_account_id = current_user.get_user_viewing_partnership_account_id

    widget = Widget.query.options(
        load_only('id')
    ).filter(Widget.id == id_, Widget.partnership_account_id == partnership_account_id).first()
    db.session.delete(widget)
    db.session.commit()
    return ''


@widgets_api.route('/api/outbound/<int:id>', methods=['GET'])
@login_required
def get_widget(id):
    """ Retrieves a single widget with the given ID. """
    try:
        # Check if being viewed by super partner
        partnership_account_id = current_user.partnership_account_id

        if current_user.is_viewing_partnership:
            partnership_account_id = current_user.get_user_viewing_partnership_account_id

        widget = Widget.query.filter(
            and_(Widget.id == id, Widget.partnership_account_id == partnership_account_id)
        ).one()
        widget.options['name'] = widget.name
        widget.options['guid'] = widget.guid
        widget.options['id'] = widget.id
        widget.options['fromNumberId'] = widget.inbound_id

        return jsonify(widget.options)
    except NoResultFound:
        return make_response('Widget not found.', 404)


@widgets_api.route('/api/outbound/<int:id>', methods=['PUT'])
@login_required
def change_widget(id):
    """ Changes a widget with the given id. """
    json = request.get_json()

    # Check if being viewed by super partner
    partnership_account_id = current_user.partnership_account_id

    if current_user.is_viewing_partnership:
        partnership_account_id = current_user.get_user_viewing_partnership_account_id

    widget = Widget.query.filter(
        and_(Widget.id == id, Widget.partnership_account_id == partnership_account_id)
    ).one()
    widget.name = json['name']
    widget.options = json

    phone = Phone.query.filter(
        Phone.id == json['fromNumberId'], Phone.partnership_account_id == partnership_account_id
    ).first()
    if phone:
        widget.inbound = phone
    else:
        widget.inbound, widget.inbound_id = None, None
    db.session.commit()

    return jsonify(id=widget.id, name=widget.name)


@widgets_api.route('/api/outbound/phonenumbers', methods=['GET'])
@login_required
@subscription_required
def phonenumbers():
    """ Retrieves the list of all phone numbers which the user is entitled to use
    in his widgets.
    """

    # Check if being viewed by super partner
    partnership_account_id = current_user.partnership_account_id

    if current_user.is_viewing_partnership:
        partnership_account_id = current_user.get_user_viewing_partnership_account_id

    result = db.session.query(Phone.id, Phone.type, Phone.phonenumber, Phone.friendly_name).filter(
        Phone.partnership_account_id == partnership_account_id,
        Phone.type.in_(('tracking', 'priority')),
        Phone.is_deactivated.is_(False)
    ).all()

    return jsonify(data=[
        {"id": n[0], "type": n[1], "number": n[2], "friendly_name": n[3]} for n in result
    ])


@widgets_api.route('/api/outbound/settings/<guid>', methods=['GET'])
@cross_origin()
def get_settings(guid):
    """ Called by the deployed widget to retrieve display settings.
    """
    try:
        from buyercall.blueprints.partnership.models import PartnershipAccount
        widget = Widget.query.join(
            Widget.partnership_account
        ).join(
            PartnershipAccount.partnership
        ).join(
            *Widget.agents.attr
        ).filter(
            Widget.guid == guid
        ).one()

        if not widget.enabled:
            return 'Widget disabled.', 403

        subscription = widget.partnership_account.subscription
        if not subscription:
            subscription = widget.partnership_account.partnership.subscription

        if subscription.usage_over_limit:
            log.info('Available minutes exceeded for partnership_account {}'.format(widget.partnership_account_id))
            return 'Available minutes exceeded.', 402

        # Sanitize widget information
        result = dict(widget.options)
        result['agentsAvailable'] = widget.agents_available

        for key in [
            'agents', 'scheduleTimezone', 'days', 'availableFrom',
            'availableTo', 'recordCalls', 'retryRouting', 'routeInSequence',
            'routeRandomly', 'routeSimultaneously', 'voicemail',
            'voicemailMessage', 'whisperMessage'
        ]:
            if key in result:
                del result[key]

        return jsonify(result)
    except NoResultFound:
        return make_response(
            'Widget not found, or has no agents assigned.', 404)


@widgets_api.route('/api/call', methods=['POST'])
@csrf.exempt
@cross_origin()
def call():
    """ The endpoint that gets called when the lead presses 'Talk!' on the
    widget.

    Receives the widget GUID as a query string parameter, and the user's data
    in the JSON body.
    """
    from buyercall.blueprints.partnership.models import PartnershipAccount
    # Save the lead
    guid = request.args.get('guid')
    json = request.get_json()
    if guid:
        widget = Widget.query.outerjoin(
            (AgentAssignment, Widget.assignments)
        ).outerjoin(
            (Agent, AgentAssignment.agent)
        ).join(Widget.partnership_account).join(PartnershipAccount.partnership).filter(Widget.guid == guid).first()
    else:
        log.error("No guid provided")
        return jsonify(success=False)

    log.info("The call widget json request is: {}".format(json))
    if json is not None:
        try:
            if widget and 'firstName' in json and json['firstName'] in ['', ' ']\
                    or 'phoneNumber' in json and json['phoneNumber'] in ['', ' ']:
                log.error('No lead fields provided for call widget ' + str(widget.name) + ' - ' + str(widget.guid) + '.')
        except:
            log.error('No lead fields provided for call widget ' + str(widget.name) + ' - ' + str(widget.guid) + '.')
            return jsonify(success=False)

        subscription = widget.partnership_account.subscription
        if not subscription:
            subscription = widget.partnership_account.partnership.subscription

        if subscription.usage_over_limit:
            log.warning('partnership_account {} has exceeded their quota.'.format(
                widget.partnership_account_id
            ))
            return jsonify(success=False)

        try:
            lead_on_call = phonecall_inprogress(widget, **json)

            if lead_on_call:
                return jsonify(success=True)
            else:
                lead = add_widget_lead(widget, **json)

                # import partnership information to get partnership id
                from buyercall.blueprints.partnership.models import Partnership, PartnershipAccount
                partner_account = PartnershipAccount.query \
                    .filter(PartnershipAccount.id == widget.partnership_account_id).first()
                partner = Partnership.query.filter(Partnership.id == partner_account.partnership_id).first()
                # Is it a Bandwidth or a Twilio number?
                if widget.inbound.type == 'tracking':
                    client = bw_client(partner.id, 'voice')
                    log.info('Calling Bandwidth number...')
                    BandwidthRouting(client).call_lead(widget, lead)
                else:
                    log.info('Calling Twilio number...')
                    subaccount_sid = subscription.twilio_subaccount_sid
                    client = subaccount_client(subaccount_sid, partner.id)
                    Routing(client).call_lead(widget, lead)

        except NoAgentsException:
            return jsonify(success=False, code='ERR_NO_AGENTS', lead_id=lead.id)

        return jsonify(success=True, callId=lead.id)
    else:
        return jsonify(success=False)


@widgets_api.route('/api/save_lead', methods=['POST'])
@csrf.exempt
@cross_origin()
def save_lead():
    """ Saves the lead information in the database.

    Receives the widget GUID as a query string parameter, and the user's data in the JSON body.
    """

    # Save the lead
    guid = request.args.get('guid')
    json = request.get_json()
    if guid:
        widget = Widget.query.filter(Widget.guid == guid).first()
    else:
        log.error('No guid provided')
        return jsonify(success=False)

    log.info("The after hours call widget json request is: {}".format(json))
    if json is not None:
        try:
            if widget and 'firstName' in json and json['firstName'] in ['', ' '] or \
                    'lastName' in json and json['lastName'] in ['', ' '] or \
                    'phoneNumber' in json and json['phoneNumber'] in ['', ' '] or \
                    'emailAddress' in json and json['emailAddress'] in ['', ' ']:
                log.error('No lead fields provided for widget ' + str(widget.name) + ' - ' + str(widget.guid) + '.')
        except:
            log.error('No lead fields provided for widget ' + str(widget.name) + ' - ' + str(widget.guid) + '.')
            return jsonify(success=False)

        lead = add_widget_lead(widget, status='missed', **json)

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

        # TODO: Should we schedule retries here?
        # schedule_retries(lead)

        return jsonify(success=True)
    else:
        return jsonify(success=False)


@widgets_api.route('/api/call_status/<int:lead_id>', methods=['GET'])
@csrf.exempt
@cross_origin()
def call_status(lead_id):
    # Declare redis url
    redis_db = redis.StrictRedis(
        host=current_app.config['REDIS_CONFIG_URL'],
        port=current_app.config['REDIS_CONFIG_PORT'],
        decode_responses=True
    )

    try:
        connect = redis_db.get('CONNECT{}'.format(lead_id))
        if connect == '1':
            return jsonify(callConnect=True)
        elif connect == '-1':
            return jsonify(callConnect=False, error=True)
    except Exception as e:
        log.error('Cannot retrieve lead status - is Redis accessible?')

    return jsonify(callConnect=False)


def fix_name(old_name):
    """ Queries the database for all names starting with `old_name` and ensures
    no name clash is taking place.
    """
    def name_generator():
        yield old_name
        for i in itertools.count(2):
            yield '{} {}'.format(old_name, i)

    # Check if being viewed by super partner
    # partnership_account_id = current_user.partnership_account_id
    #
    # if current_user.is_viewing_partnership:
    #     partnership_account_id = current_user.get_user_viewing_partnership_account_id

    escaped_name = old_name.replace('\\', '\\\\').replace('%', '\\%').replace(
        '_', '\\_'
    )
    same_name_widgets = Widget.query.filter(
        and_(Widget.name.like(escaped_name + '%'))
    ).all()
    db_names = set(widget.name for widget in same_name_widgets)

    for name in name_generator():
        if name not in db_names:
            return name


@widgets_api.route('/api/v1/outbound', methods=['GET'])
@rest_method
def get_outbound():
    """
    External REST API endpoint.

    Retrieves all widgets for the current user.
    """
    leads = Lead.query.filter(
        Lead.partnership_account_id == rest_partnership_account.id
    ).with_entities(
        Lead.widget_guid,
        func.sum(1).label('lead_count')
    ).group_by(Lead.widget_guid).subquery()

    all_outbound = Widget.query.options(
        load_only('id', 'name', 'created_on', 'updated_on', 'enabled')
    ).filter(
        Widget.partnership_account_id == rest_partnership_account.id
    ).outerjoin(
        (leads, Widget.guid == leads.c.widget_guid)
    ).with_entities(
        Widget.id,
        Widget.name,
        case(
            [(leads.c.lead_count.is_(None), 0)],
            else_=leads.c.lead_count
        ).label('lead_count'),
        Widget.created_on,
        Widget.updated_on,
        Widget.enabled
    ).all()

    data = []
    for o in all_outbound:
        data.append(dict(
            id=o.id,
            friendly_name=o.name,
            lead_count=o.lead_count,
            created_on=o.created_on,
            updated_on=o.updated_on,
            enabled=o.enabled
        ))

    response = make_response(jsonify(outbound=data))
    response.headers['Cache-Control'] = 'no-cache'
    return response


def set_custom_agent_lead_text(lead_id, vehicle_details, vehicle_year, vehicle_make, vehicle_model):
    """ Sets a custom message to be used as the whisper message. The custom message will mostly be used for setting
        vehicle details. The remaining fields that are set are specifically for the notification mail.
    """
    # Declare redis url
    redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
                                 port=current_app.config['REDIS_CONFIG_PORT'])

    vehicle_details = ' . . Vehicle information is . . . ' + vehicle_details
    redis_db.setex('CUSTOM_LEAD_MESSAGE:' + str(lead_id), 2 * DAYS, vehicle_details)
    redis_db.setex('CUSTOM_LEAD_YEAR:' + str(lead_id), 2 * DAYS, vehicle_year)
    redis_db.setex('CUSTOM_LEAD_MAKE:' + str(lead_id), 2 * DAYS, vehicle_make)
    redis_db.setex('CUSTOM_LEAD_MODEL:' + str(lead_id), 2 * DAYS, vehicle_model)


def set_lead_xml(lead_id, email_text):
    """ Saves the xml content in memory to be forwarded to CRM if need be.
    """
    # Declare redis url
    redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
                                 port=current_app.config['REDIS_CONFIG_PORT'])

    redis_db.setex('CUSTOM_LEAD_XML:' + str(lead_id), 2 * DAYS, email_text)