File: //home/arjun/projects/buyercall_forms/buyercall/buyercall/blueprints/phonenumbers/views.py
import os
import csv
import json
import uuid
import time
import os.path as path
from contextlib import closing
from io import StringIO
import logging as log
import xmltodict
import traceback
from datetime import date, datetime
from flask import (
Blueprint,
request,
flash,
url_for,
jsonify,
redirect,
current_app as app,
send_from_directory,
make_response,
render_template)
from flask_login import login_required, current_user
from sqlalchemy import or_, and_
from sqlalchemy.orm import defer
from sqlalchemy.sql import text
from sqlalchemy.sql.expression import func, case
from buyercall.blueprints.phonenumbers.forms import PhoneForm
from buyercall.blueprints.partnership.models import PartnershipAccount
from buyercall.blueprints.phonenumbers.models import (
HoldMusic,
Phone,
)
from buyercall.blueprints.mobile.models import Endpoint
from buyercall.blueprints.user.decorators import role_required
from flask_babel import gettext as _
from datatables import DataTables, ColumnDT
from buyercall.blueprints.billing.decorators import subscription_required
from buyercall.blueprints.filters import format_phone_number
from buyercall.extensions import csrf
from buyercall.extensions import db
from buyercall.lib.util_crypto import AESCipher
from buyercall.lib.util_twilio import (
bw_client,
account_client,
subaccount_client
)
from buyercall.lib.util_bandwidth import bw_client as bw_dashboard_client
from buyercall.lib.bandwidth import BandwidthException
from twilio.base.exceptions import TwilioRestException
from buyercall.blueprints.leads.models import Lead
from buyercall.blueprints.partnership.models import PartnershipCpaasPhoneNumberSubscriptions as NumberSubscription
phonenumbers = Blueprint('phonenumbers', __name__, template_folder='templates')
MAX_FILE_SIZE = 4 * 1024 * 1024
class ClientError(Exception):
def __init__(self, message):
self.message = message
# Inbound Routing onboarding page
@phonenumbers.route('/inbound_onboarding', methods=['GET', 'POST'])
@login_required
@role_required('admin', 'partner', 'sysadmin')
def onboarding_inbound():
if request.method == 'POST':
current_user.inbound_onboard = True
db.session.commit()
if current_user.inbound_onboard:
flash(_('Great, you are ready to get started with the Inbound Routing '
'Wizard feature. Remember to checkout the support section or '
'FAQ if you have any additional inbound routing questions.'),
'success')
return redirect(url_for('phonenumbers.inbound_list'))
return render_template('phonenumbers/inbound_onboarding.jinja2')
@phonenumbers.route('/inbound/install-instructions/<int:id>', methods=['GET'])
@login_required
@role_required('admin')
def install_instructions(id):
partnership_account_id = current_user.partnership_account_id
is_admin_in_group = current_user.is_admin_user_with_groups
viewing_partnership_account = current_user.is_viewing_partnership
# Check if being viewed by super partner
if is_admin_in_group and viewing_partnership_account:
partnership_account_id = current_user.get_user_viewing_partnership_account_id
elif not viewing_partnership_account and is_admin_in_group:
return redirect(url_for('partnership.company_accounts'))
inbound = Phone.query.filter(
Phone.id == id, Phone.partnership_account_id == partnership_account_id
).first()
if inbound is None:
return redirect(url_for('.inbound_list'))
greeting_messages = 'Custom audio file' if (
inbound.routing_config.get('whisperMessageType', '') == 'audio'
) else 'Text to speech'
retry_count = ['Never', 'Once', 'Twice'][inbound.routing_config['defaultRouting']['retryRouting']]
notify_leads = inbound.notifications.get('notifyLeads')
if notify_leads == 'missed':
notifications = 'Missed calls only'
elif notify_leads == 'all':
notifications = 'All calls'
else:
notifications = 'Disabled'
auto_attendance = 'Enabled' if (
inbound.routing_config.get('routingType') == 'digits'
) else 'Disabled'
call_recording = 'Enabled' if (
inbound.routing_config.get('recordCalls')
) else 'Disabled'
call_backs = 'Enabled' if (
inbound.routing_config.get('callBack')
) else 'Disabled'
if call_backs == 'Enabled':
call_back_intervals = ', '.join('{} min'.format(x) for x in [
inbound.routing_config.get('firstCallBack'),
inbound.routing_config.get('secondCallBack'),
inbound.routing_config.get('thirdCallBack'),
] if x)
else:
call_back_intervals = 'Disabled'
return render_template(
'phonenumbers/inbound_instructions.jinja2', id=id,
phonenumber=inbound.phonenumber,
friendlyname=inbound.friendly_name,
source=inbound.source,
type=inbound.type,
status=inbound.active,
agents_list=inbound.agents_list,
mobile_agent_email=inbound.mobile_agent_email,
mobile_agent_id=inbound.mobile_agent_id,
greeting_messages=greeting_messages,
retry_count=retry_count,
notifications=notifications,
notification_recipients=', '.join(inbound.notification_recipients),
auto_attendance=auto_attendance,
call_recording=call_recording,
voicemail='Enabled' if inbound.routing_config.get('voicemail') else 'Disabled',
hold_music='Enabled' if inbound.routing_config.get('playHoldMusic') else 'Disabled',
custom_hold_music='Enabled' if inbound.routing_config.get('customHoldMusic') else 'Disabled',
call_backs=call_backs,
call_back_intervals=call_back_intervals,
)
@phonenumbers.route('/inbound/email-instructions/<int:id>', methods=['POST'])
@login_required
@role_required('admin')
def email_instructions(id):
from .tasks import send_phonenumber_email
partnership_account_id = current_user.partnership_account_id
is_admin_in_group = current_user.is_admin_user_with_groups
viewing_partnership_account = current_user.is_viewing_partnership
# Check if being viewed by super partner
if is_admin_in_group and viewing_partnership_account:
partnership_account_id = current_user.get_user_viewing_partnership_account_id
elif not viewing_partnership_account and is_admin_in_group:
return redirect(url_for('partnership.company_accounts'))
try:
inbound = Phone.query.filter(
Phone.id == id, Phone.partnership_account_id == partnership_account_id
).first()
if not inbound:
return redirect(url_for('.inbound_list'))
email = request.form['email']
send_phonenumber_email.delay(inbound.phonenumber, email, inbound.partnership_account_id)
except Exception:
log.error(traceback.format_exc())
flash(
_('Sorry, we were unable to send the email.'), 'danger'
)
finally:
return redirect(url_for('.install_instructions', id=id))
@phonenumbers.route('/inbound')
@login_required
@role_required('admin')
def inbound_list():
partnership_account_id = current_user.partnership_account_id
is_admin_in_group = current_user.is_admin_user_with_groups
viewing_partnership_account = current_user.is_viewing_partnership
# Check if being viewed by super partner
if is_admin_in_group and viewing_partnership_account:
partnership_account_id = current_user.get_user_viewing_partnership_account_id
elif not viewing_partnership_account and is_admin_in_group:
return redirect(url_for('partnership.company_accounts'))
# Check if onboarding was excepted
if current_user.inbound_onboard is False:
return redirect(url_for('phonenumbers.onboarding_inbound'))
filter_by = partnership_account_id == Phone.partnership_account_id
# defining the initial query depending on your purpose
paginated_phone = Phone.query.filter(filter_by).count()
subscription = current_user.subscription
subscriped = subscription is not None and subscription.status == 'active'
return render_template('phonenumbers/phonenumbers.jinja2',
phonenumbers=paginated_phone,
subscribed=subscriped)
# return agent data into jquery datatables
@phonenumbers.route('/phone-data')
@csrf.exempt
@login_required
@role_required('admin')
def data():
"""Return server side data."""
# defining columns
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))
columns = [
'id', 'friendly_name', 'phonenumber', 'type', 'lead_count',
'created_on', 'updated_on'
]
partnership_account_id = current_user.partnership_account_id
viewing_partnership_account = current_user.is_viewing_partnership
# Check if being viewed by super partner
if viewing_partnership_account:
partnership_account_id = current_user.get_user_viewing_partnership_account_id
total = Phone.query.filter(
(Phone.partnership_account_id == partnership_account_id),
Phone.is_deactivated.is_(False)
)
leads = Lead.query.filter(
Lead.partnership_account_id == partnership_account_id
).with_entities(
Lead.inbound_id,
func.sum(1).label('lead_count')
).group_by(Lead.inbound_id).subquery()
filtered = total
if search:
pattern = '%{}%'.format(search)
filtered = total.filter(or_(
Phone.friendly_name.ilike(pattern),
Phone.phonenumber.like(pattern),
Phone.toll_type.ilike(pattern)
))
filtered = filtered.outerjoin(
(leads, Phone.id == leads.c.inbound_id)
).with_entities(
Phone.id,
Phone.friendly_name,
Phone.phonenumber,
Phone.toll_type.label('type'),
case(
[(leads.c.lead_count.is_(None), 0)],
else_=leads.c.lead_count
).label('lead_count'),
Phone.created_on,
Phone.updated_on
)
sorted_ = filtered
if 0 <= order < len(columns):
order_pred = '{} {}'.format(columns[order], direction)
sorted_ = sorted_.order_by(text(order_pred))
sorted_ = sorted_.offset(offset).limit(limit)
data = [
{i: col for i, col in enumerate(row)} for row in sorted_.all()
]
return jsonify(
draw=request.args['draw'],
recordsFiltered=filtered.count(),
recordsTotal=total.count(),
data=data
)
# return agent data into jquery datatables
@phonenumbers.route('/inbound/csv')
@login_required
@role_required('admin')
def data_csv():
"""Return server side data."""
# defining columns
header = [
'No', 'Friendly Name', 'Phone Number', 'Type', 'Lead Count',
'Created On', 'Updated On'
]
fields = [
'id', 'friendly_name', 'phonenumber', 'type', 'lead_count',
'created_on', 'updated_on'
]
columns = [
ColumnDT(Phone.id),
ColumnDT(Phone.friendly_name),
ColumnDT(Phone.phonenumber),
ColumnDT(Phone.type),
ColumnDT(Phone.lead_count),
ColumnDT(Phone.created_on),
ColumnDT(Phone.updated_on)
]
partnership_account_id = current_user.partnership_account_id
viewing_partnership_account = current_user.is_viewing_partnership
# Check if being viewed by super partner
if viewing_partnership_account:
partnership_account_id = current_user.get_user_viewing_partnership_account_id
# TODO: defer_by routing_config
query = db.session.query().select_from(Phone).group_by(Phone.id).filter(
partnership_account_id == Phone.partnership_account_id
)
row_table = DataTables(request.args, query, columns)
result = row_table.output_result()
# Build the CSV
row_no = 0
with closing(StringIO()) as out:
writer = csv.writer(out)
writer.writerow(header)
for row in result['data']:
csv_row = [row[key] for key in sorted(row.keys())]
row_no += 1
csv_row[0] = row_no
writer.writerow(csv_row)
filename = 'Buyercall Inbound Routings - {}.csv'.format(
date.today().strftime('%Y-%m-%d')
)
resp = make_response(out.getvalue())
resp.headers['Content-Type'] = 'text/csv'
resp.headers['Content-Disposition'] = \
'attachment; filename="{}"'.format(filename)
return resp
@phonenumbers.route('/inbound/new', methods=['GET'])
@login_required
@subscription_required
@role_required('admin')
def inbound_new():
phone = Phone()
form = PhoneForm(obj=phone)
partnership_account_id = current_user.partnership_account_id
is_admin_in_group = current_user.is_admin_user_with_groups
viewing_partnership_account = current_user.is_viewing_partnership
# Check if being viewed by super partner
if is_admin_in_group and viewing_partnership_account:
partnership_account_id = current_user.get_user_viewing_partnership_account_id
elif not viewing_partnership_account and is_admin_in_group:
return redirect(url_for('partnership.company_accounts'))
total_phonenumbers = Phone.query.filter(
partnership_account_id == Phone.partnership_account_id
).count()
# check only for phonenumbers that's not deactivated
total_active_phonenumbers = Phone.query.filter(
partnership_account_id == Phone.partnership_account_id
).filter(Phone.is_deactivated == '0').count()
from buyercall.blueprints.agents.models import Agent
partner_agent = Agent.query \
.filter(Agent.partnership_account_id == partnership_account_id).first()
partner_agent_phone = format_phone_number(partner_agent.phonenumber)
# Check to see if user exceeded his plan's phonenumber limit
subscription = current_user.partnership_account.subscription
# Set the plan limit as a variable
plan_number_limit = subscription.phonenumber_limit
if plan_number_limit <= total_active_phonenumbers:
log.info('the subscription phone number limit is {}'.format(plan_number_limit))
log.info('the current phone number total is {}'.format(total_active_phonenumbers))
if current_user.subscription.plan == 'partnershipsingle':
flash(_(
'You have reached your plans phone number limit. Please <a href='"/support"' style='"color:#ffffff"'>'
'<strong>contact BuyerCall support</strong></a> '
'if you are interested in more phone number and call features. '), 'warning')
return redirect(url_for('phonenumbers.inbound_list'))
else:
flash(_(
'You have reached your plans phone number limit. '
'Please <a href='"/subscription/update"' style='"color:#ffffff"'>'
'<strong>Upgrade your plan</strong></a> to active more phone numbers '), 'danger')
return redirect(url_for('phonenumbers.inbound_list'))
log.info('the subscription phone number limit is {}'.format(plan_number_limit))
log.info('the current phone number total is {}'.format(total_active_phonenumbers))
# Check to see how many priority phone numbers are used and check it against plan limit
# check only for phonenumbers that's not deactivated
total_active_priority_numbers = Phone.query.filter(
partnership_account_id == Phone.partnership_account_id
).filter(Phone.is_deactivated == '0').filter(Phone.type == 'priority').count()
# Set the plan's prioirty number limit as variable
plan_priority_limit = subscription.priority_number_limit
log.info('the user has {} active priority numbers'.format(total_active_priority_numbers))
log.info('the subscription phone number limit is {}'.format(plan_priority_limit))
return render_template(
'phonenumbers/wizard.jinja2', form=form,
phone=phone, total_phonenumbers=total_phonenumbers,
total_active_priority_numbers=total_active_priority_numbers,
plan_priority_limit=plan_priority_limit,
agent=partner_agent,
agent_phone=partner_agent_phone,
)
@phonenumbers.route('/inbound/<int:id>', methods=['GET'])
@login_required
@subscription_required
@role_required('admin')
def phonenumber_edit(id):
partnership_account_id = current_user.partnership_account_id
is_admin_in_group = current_user.is_admin_user_with_groups
viewing_partnership_account = current_user.is_viewing_partnership
# Check if being viewed by super partner
if is_admin_in_group and viewing_partnership_account:
partnership_account_id = current_user.get_user_viewing_partnership_account_id
elif not viewing_partnership_account and is_admin_in_group:
return redirect(url_for('partnership.company_accounts'))
routing = Phone.query.filter(
Phone.partnership_account_id == partnership_account_id,
Phone.id == id,
).first()
if not routing:
flash('Inbound routing with ID {} not found.'.format(id))
return redirect(url_for('phonenumbers.inbound_list'))
# This variable is to state if the leads name gets whisper to the agent
# If it's set to true it will be whispered. If false no whisper. By default
# its false.
caller_whisper = routing.caller_id
from buyercall.blueprints.agents.models import Agent
partner_agent = Agent.query \
.filter(Agent.partnership_account_id == partnership_account_id).first()
partner_agent_phone = format_phone_number(partner_agent.phonenumber)
routing_data = routing.as_dict()
log.info('The pre-routing data is: {}'.format(routing_data))
if routing_data['routingConfig'].get('configSMSSetup', '') and \
routing_data['routingConfig'].get('SMSAutoReply', '') \
and routing_data['routingConfig'].get('SMSAutoReplyImageUrl', ''):
mms_url = routing_data['routingConfig'].get('SMSAutoReplyImageUrl', '')
media_key = (mms_url.split('/', 3)[-1]).split('?')[0].replace('%20', ' ')
bucket = app.config['MMS_BUCKET']
from buyercall.lib.util_boto3_s3 import generate_presigned_aws_url
presigned_mms_url = generate_presigned_aws_url(media_key, bucket)
routing_data['routingConfig']['SMSAutoReplyImageUrl'] = presigned_mms_url
log.info('The post-routing data is: {}'.format(routing_data))
json_routing_data = json.dumps(routing_data).replace('</', '<\\/')
return render_template(
'phonenumbers/wizard.jinja2', model_data=json_routing_data,
phone=routing,
caller_whisper=caller_whisper,
agent=partner_agent,
agent_phone=partner_agent_phone,
)
@phonenumbers.route('/inbound/hold-music', methods=['POST'])
@login_required
@subscription_required
@role_required('admin', 'partner', 'sysadmin')
def upload_hold_music():
def allowed_file(file_ext):
return file_ext in ['.mp3', '.wav']
file = request.files['file']
file_ext = path.splitext(file.filename.lower())[1]
if not file:
return make_response('Error uploading file.', 400)
if not allowed_file(file_ext):
return make_response('File extension not allowed: {}'.format(file_ext), 400)
if file.content_length and file.content_length > MAX_FILE_SIZE:
return make_response('File too large.', 400)
file_guid = str(uuid.uuid4())
file_path = path.join(app.config['UPLOAD_FOLDER'], file_guid)
file.save(file_path)
if path.getsize(file_path) > MAX_FILE_SIZE:
os.remove(file_path)
return make_response('File too large.', 400)
partnership_account_id = current_user.partnership_account_id
viewing_partnership_account = current_user.is_viewing_partnership
# Check if being viewed by super partner
if viewing_partnership_account:
partnership_account_id = current_user.get_user_viewing_partnership_account_id
hold_music = HoldMusic(
partnership_account_id=partnership_account_id,
uuid=file_guid,
filename=file.filename
)
db.session.add(hold_music)
db.session.commit()
from .bw_tasks import bw_upload_holdmusic
bw_upload_holdmusic(current_user.id, hold_music.id, file_path)
return jsonify(guid=file_guid, filename=file.filename)
@phonenumbers.route('/inbound/hold-music/<file_guid>', methods=['GET'])
def hold_music(file_guid):
file_dir = path.join(app.config['UPLOAD_FOLDER'], 'hold_music')
return send_from_directory(file_dir, file_guid, mimetype='audio/mpeg')
@phonenumbers.route('/api/inbound/routings', methods=['GET'])
@login_required
@subscription_required
@role_required('admin', 'partner', 'sysadmin')
def get_routings():
return jsonify(data=[])
@phonenumbers.route('/api/inbound/routings', methods=['POST'])
@login_required
@subscription_required
@role_required('admin')
def create_routing():
current_partnership_account_id = current_user.partnership_account_id
viewing_partnership_account = current_user.is_viewing_partnership
# Check if being viewed by super partner
if viewing_partnership_account:
current_partnership_account_id = current_user.get_user_viewing_partnership_account_id
model = request.json
routing = Phone(
partnership_account_id=current_partnership_account_id
)
routing.phonenumber = model['phoneNumber']
routing.friendly_name = model['friendlyName']
routing.source = model['source']
routing.channel = model['channel']
routing.caller_id = model['callerId']
routing.routing_config = model['routingConfig']
routing.notifications = model['notificationConfig']
routing.type = model['type']
routing.local = (model['typePn'] == 'local')
routing.tollfree = (model['typePn'] == 'tollfree')
provider = 'bandwidth' if model['type'] in ['tracking', 'mobile'] else 'twilio'
routing.provider = provider
try:
log.info('Purchasing number from {}'.format(provider))
if provider == 'bandwidth':
if model['type'] == 'mobile':
# Auto-generate a sip username and password for mobile app use
username = routing.mobile_agent_email.split('@')[0]
username_hex = uuid.uuid4().hex[:5]
sip_username = '{}{}_{}'.format(username, username_hex, routing.partnership_account_id)
# Create a random password string used for mobile passwords
random_pwd = uuid.uuid4().hex[:23].lower().replace('0', 'X').replace('o', 'Y').replace('e', 'E')
sip_password = random_pwd
# Provision the mobile phone number using the sip username and password
purchase_bw_mobile(routing, sip_username, sip_password)
else:
purchase_bw(routing)
elif provider == 'twilio':
purchase_twilio(routing)
routing.connect_audio_files()
return jsonify(routing.as_dict())
except Exception:
log.error(traceback.format_exc())
db.session.rollback()
return 'Error while purchasing phone number', 500
def endpoint_exists_check_bw(partnership_id, sip_username):
exists = False
client = bw_client(partnership_id)
# Lets get the domain now using the partnership_id
from ..mobile.models import Domain
domain = Domain.query\
.filter(and_(Domain.is_deactivated == False, Domain.partnership_id == partnership_id))\
.first()
if domain is not None:
# Check to see if the username exists in the endpoints
found_endpoints = client.domains.get_endpoints(domain_id=domain.domain_id)
if found_endpoints is not None:
for endpoint in found_endpoints:
if 'credentials' in endpoint:
cred = endpoint['credentials']
if 'username' in cred:
username = cred['username']
if username == sip_username:
exists = True
break
return exists
def purchase_bw(routing, api_call=False, partnership_account=None):
# Check to see if it is an api_call. If it is, check the partner/account details.
if api_call and partnership_account is not None:
log.info('Purchasing via API call. Using API token details.')
partnership_account_id = partnership_account.id
else:
log.info('Purchasing via frontend. Using current_user details.')
partnership_account_id = current_user.partnership_account_id
# import partnership information to get partnership id
from buyercall.blueprints.partnership.models import Partnership, PartnershipAccount, PartnershipCpaasProviders
partner_account = PartnershipAccount.query \
.filter(PartnershipAccount.id == partnership_account_id).first()
dashboard_client = bw_dashboard_client(partner_account.partnership_id)
# Check to see if there's a phone number order subscription available for the partnership
current_subscription = NumberSubscription.query\
.filter(NumberSubscription.order_type == 'orders').first()
# If there's no current subscription we need to create one and save it to the database
if not current_subscription:
call_back_url = 'https://{}{}'.format(
app.config.get('SERVER_DOMAIN'),
url_for('phonenumbers.subscription_callback')
)
create_subscription = dashboard_client.subscriptions.subscribe(order_type='orders',
callback_url=call_back_url,
expiry='3153600000')
subscription_id = create_subscription.rsplit('/', 1)[-1]
# Save the new order subscription to the database
subscription = NumberSubscription(
cpaas_provider_name='bandwidth',
partnership_id=partner_account.partnership_id,
order_type='orders',
subscription_id=subscription_id,
callback_url=call_back_url
)
db.session.add(subscription)
db.session.commit()
log.info('A new subscription has been added for phone number orders')
# We don't want to purchase phone numbers when debugging/testing
if app.config.get('DEBUG', False):
log.warning('DEBUG is enabled; not purchasing the number.')
# Just take our first defined phone number to get a fake Id
test_number = app.config['BANDWIDTH_CALLER_ID']
test_order_id = app.config['BANDWIDTH_ORDER_ID']
valid_test_number = dashboard_client.in_service_number.validate(test_number)
if valid_test_number == 200:
bw_number_id = test_order_id
# Use the config bandwidth number as phone number for testing purposes
routing.phonenumber = format_phone_number(app.config['BANDWIDTH_CALLER_ID'])
else:
flash(_('The bandwidth test number does not exist. A 404 error was received.'), 'danger')
log.info('The bandwidth test number does not exist. A 404 error was received.')
return ''
else:
# Order a phone number in the Dashboard platform. Below we will import it into the
# Application platform after creating an application
unformatted_phone_number = routing.phonenumber
# We need to remove the +1 from the phone number, because the Dashboard Order API doesn't allow for it
bw_pn_formatted = unformatted_phone_number.replace("+1", "")
log.info('The phone number passed to the BW Dashboard Order API is: {}'.format(bw_pn_formatted))
order_bw_number = dashboard_client.phone_numbers.order(partner_account_id=str(partnership_account_id),
name='{}:{} - ({}) - {}'.format(
partnership_account_id,
routing.id,
routing.type,
routing.friendly_name
),
phonenumber=bw_pn_formatted)
# Get the order id from the location url by splitting the url
bw_number_id = order_bw_number.rsplit('/', 1)[-1]
# If the sms config is set to True lets set a messaging url variable otherwise leave it empty
if routing.routing_config.get("configSMSSetup", False):
sms_enabled = 'on'
else:
sms_enabled = 'off'
# We need to delay the updating of TN to enable/disable SMS. We delay with celery task
from .bw_tasks import update_tn
update_tn.apply_async(args=[str(partnership_account_id), partner_account.partnership_id,
bw_pn_formatted, 'tracking', sms_enabled], countdown=180)
routing.twilio_number_sid = bw_number_id
# Save the phone number and routing to the database
db.session.add(routing)
db.session.commit()
def purchase_bw_mobile(routing, sip_username, sip_password, api_call=False, api_sip_description=None, partnership_account=None):
# Check to see if it is an api_call. If it is, check the partner/account details.
if api_call and partnership_account is not None:
log.info('Purchasing via API call. Using API token details.')
partnership_account_id = partnership_account.id
else:
log.info('Purchasing via frontend. Using current_user details.')
partnership_account_id = current_user.partnership_account_id
# import partnership information to get partnership id
from buyercall.blueprints.partnership.models import Partnership, PartnershipAccount, PartnershipCpaasProviders
partner_account = PartnershipAccount.query \
.filter(PartnershipAccount.id == partnership_account_id).first()
partner = Partnership.query.filter(Partnership.id == partner_account.partnership_id).first()
dashboard_client = bw_dashboard_client(partner.id, tn_type='mobile')
# Check to see if there's a phone number order subscription available for the partnership
current_subscription = NumberSubscription.query \
.filter(NumberSubscription.order_type == 'orders').first()
# If there's no current subscription we need to create one and save it to the database
if not current_subscription:
call_back_url = 'https://{}{}'.format(
app.config.get('SERVER_DOMAIN'),
url_for('phonenumbers.subscription_callback')
)
create_subscription = dashboard_client.subscriptions.subscribe(
order_type='orders',
callback_url=call_back_url,
expiry='3153600000'
)
subscription_id = create_subscription.rsplit('/', 1)[-1]
# Save the new order subscription to the database
subscription = NumberSubscription(
cpaas_provider_name='bandwidth',
partnership_id=partner.id,
order_type='orders',
subscription_id=subscription_id,
callback_url=call_back_url
)
db.session.add(subscription)
db.session.commit()
log.info('A new subscription has been added for phone number orders')
# We don't want to purchase phone numbers when debugging/testing
if app.config.get('DEBUG', False):
log.warning('DEBUG is enabled; not purchasing the number.')
# Just take our first defined phone number to get a fake Id
test_number = app.config['BANDWIDTH_CALLER_ID_MOBILE']
test_order_id = app.config['BANDWIDTH_ORDER_ID']
valid_test_number = dashboard_client.in_service_number.validate(test_number)
if valid_test_number == 200:
bw_number_id = test_order_id
# Use the config bandwidth number as phone number for testing purposes
routing.phonenumber = format_phone_number(app.config['BANDWIDTH_CALLER_ID_MOBILE'])
else:
flash(_('The bandwidth test number does not exist. A 404 error was received.'), 'danger')
log.info('The bandwidth test number does not exist. A 404 error was received.')
return ''
else:
# Order a phone number in the Dashboard platform. Below we will import it into the
# Application platform after creating an application
unformatted_phone_number = routing.phonenumber
# We need to remove the +1 from the phone number, because the Dashboard Order API doesn't allow for it
bw_pn_formatted = unformatted_phone_number.replace("+1", "")
log.info('The mobile number passed to the BW Dashboard Order API is: {}'.format(bw_pn_formatted))
order_bw_number = dashboard_client.phone_numbers.order(partner_account_id=str(partnership_account_id),
name='{}:{} - ({}) - {}'.format(
partnership_account_id,
routing.id,
routing.type,
routing.friendly_name
),
phonenumber=bw_pn_formatted)
# Get the order id from the location url by splitting the url
bw_number_id = order_bw_number.rsplit('/', 1)[-1]
# Always enable SMS for mobile numbers
sms_enabled = 'on'
# We need to delay the updating of TN to enable/disable SMS. We delay with celery task
from .bw_tasks import update_tn
update_tn.apply_async(args=[str(partnership_account_id), partner_account.partnership_id,
bw_pn_formatted, 'mobile', sms_enabled], countdown=180)
routing.twilio_number_sid = bw_number_id
# Save the imported phone number and routing to the database
db.session.add(routing)
db.session.commit()
# Lets get the domain now using the partnership_id
from buyercall.blueprints.mobile.models import Domain
# This is the same as realm,
realm = Domain.query\
.filter(and_(Domain.is_deactivated.is_(False), Domain.partnership_id == partner.id)).first()
# Lets create a sip endpoint now
if api_call and api_sip_description is not None and api_sip_description is not '':
sip_description = api_sip_description
else:
sip_description = '{} sip domain endpoint for mobile account use'.format(realm.name)
endpoint = dashboard_client.realms.create_sip_credentials(
realm_id=realm.domain_id,
realm=realm.sip_realm,
sip_username=sip_username,
sip_password=sip_password
)
# Lets get the information on the newly created sip endpoint
# get_endpoint = client.domains.get_endpoint(domain_id=realm.domain_id, endpoint_id=endpoint.id)
# log.info('The endpoint info is {}'.format(get_endpoint))
log.info('The sip response is: {}'.format(endpoint.content.decode()))
print(endpoint.status_code)
if 300 >= endpoint.status_code:
endpoint_content = xmltodict.parse(endpoint.content.decode())
json_endpoint_content = json.loads(json.dumps(endpoint_content))
endpoint_id = json_endpoint_content['SipCredentialsResponse']['ValidSipCredentials']['SipCredential']['Id']
endpoint_uri = 'sip:' + sip_username + '@' + realm.sip_realm
endpoint_username = json_endpoint_content['SipCredentialsResponse']['ValidSipCredentials']['SipCredential']['UserName']
endpoint_hash_1 = json_endpoint_content['SipCredentialsResponse']['ValidSipCredentials']['SipCredential']['Hash1']
endpoint_hash_1_b = json_endpoint_content['SipCredentialsResponse']['ValidSipCredentials']['SipCredential']['Hash1b']
log.info('The endpoint id is: {}'.format(endpoint_id))
# Get the agent assigned to the SIP from the routing configuration
agents = routing.routing_config.get("defaultRouting").get("agents")
agent_id = agents[0].get("id")
# Create a SIP endpoint in BuyerCall for the specific phone number and application created above
Endpoint.create(sip_description, realm.provider, endpoint_id, endpoint_uri, sip_username,
sip_password, True, routing.id, agent_id, partnership_account_id, realm.id, False,
hash_1=endpoint_hash_1, hash_1_b=endpoint_hash_1_b)
# Return the newly created sip endpoint info
sip_endpoint = Endpoint.query\
.filter(and_(Endpoint.sip_username == endpoint_username,
Endpoint.is_deactivated.is_(False))).first()
# Generate a QR code for the sip endpoint account so that they can login to app
log.info('The partner name is {}'.format(partner_account.name))
from buyercall.blueprints.mobile.tasks import qr_code
qr = qr_code(sip_endpoint.sip_username,
sip_endpoint.sip_password,
partner_account.id,
partner_account.name,
realm.cloud_id)
# Save the QR link to the sip endpoint database
sip_endpoint.qr_url = qr
db.session.commit()
else:
log.info('Unable to create endpoint for sip username {} and account {}'
.format(sip_username, partner_account.id))
return ''
def purchase_twilio(routing, api_call=False, partnership_account=None):
# Check to see if it is an api_call. If it is, check the partner/account details.
if api_call and partnership_account is not None:
partnership_account_id = partnership_account.id
log.info('Purchasing via API call. Using API token details.')
if partnership_account.subscription is not None:
twilio_subaccount_sid = partnership_account.subscription.twilio_subaccount_sid
elif partnership_account.partnership.subscription is not None:
twilio_subaccount_sid = partnership_account.subscription.twilio_subaccount_sid
else:
log.info('Purchasing via frontend. Using current_user details.')
twilio_subaccount_sid = current_user.subscription.twilio_subaccount_sid
partnership_account_id = current_user.partnership_account_id
# import partnership information to get partnership id
from buyercall.blueprints.partnership.models import Partnership, PartnershipAccount
partner_account = PartnershipAccount.query \
.filter(PartnershipAccount.id == partnership_account_id).first()
partner = Partnership.query.filter(Partnership.id == partner_account.partnership_id).first()
client = subaccount_client(twilio_subaccount_sid, partner.id)
# We don't want to purchase phone numbers when debugging/testing
if app.config.get('DEBUG', False):
log.warning('DEBUG is enabled; not purchasing the number.')
twilio_number = client.incoming_phone_numbers.list()[0]
# twilio_number = twilio_number[0] if twilio_number else None
else:
# TODO: Fix Twilio purchasing
if routing.local:
twilio_number = client.incoming_phone_numbers.local.create(
phone_number=routing.phonenumber
)
else:
twilio_number = client.incoming_phone_numbers.toll_free.create(
phonenumber=routing.phonenumber
)
routing.twilio_number_sid = twilio_number.sid
routing.phonenumber = twilio_number.phone_number
numbers = client.incoming_phone_numbers.list(
phone_number=twilio_number.phone_number
)
db.session.add(routing)
db.session.commit()
numbers[0].update(
voice_url=url_for(
'twilio_inbound.lead_inbound_call',
inbound_id=routing.id,
_external=True,
_scheme='https'
),
status_callback=url_for(
'twilio_inbound.call_result_callback',
_external=True,
_scheme='https'
),
sms_url=url_for(
'tw_sms_inbound.lead_inbound_message',
inbound_id=routing.id,
_external=True,
_scheme='https'
),
voice_caller_id_lookup=True
)
@phonenumbers.route('/api/inbound/routings/<int:id>', methods=['GET'])
def get_routing(id):
partnership_account_id = current_user.partnership_account_id
viewing_partnership_account = current_user.is_viewing_partnership
# Check if being viewed by super partner
if viewing_partnership_account:
partnership_account_id = current_user.get_user_viewing_partnership_account_id
routing = Phone.query.filter(
Phone.partnership_account_id == partnership_account_id,
Phone.id == id,
).one()
return jsonify(routing.as_dict())
@phonenumbers.route('/api/inbound/routings/<int:id_>', methods=['PUT'])
@login_required
@subscription_required
@role_required('admin', 'partner', 'sysadmin')
def update_routing(id_):
partnership_account_id = current_user.partnership_account_id
viewing_partnership_account = current_user.is_viewing_partnership
# Check if being viewed by super partner
if viewing_partnership_account:
partnership_account_id = current_user.get_user_viewing_partnership_account_id
model = request.json
log.info(f'request data is:{model}')
routing = Phone.query.filter(
Phone.partnership_account_id == partnership_account_id,
Phone.id == id_,
).one()
routing.phonenumber = model['phoneNumber']
routing.friendly_name = model['friendlyName']
routing.routing_config = model['routingConfig']
routing.caller_id = model['callerId']
routing.notifications = model['notificationConfig']
db.session.commit()
routing.connect_audio_files()
# import partnership information to get partnership id
partner_account = PartnershipAccount.query \
.filter(PartnershipAccount.id == partnership_account_id).first()
# Only update the TN in Bandwidth if DEBUG is false
if not app.config.get('DEBUG'):
# If the sms config is set to True lets set a messaging url variable otherwise leave it empty
if routing.routing_config.get("configSMSSetup") or routing.type == 'mobile':
sms_enabled = 'on'
else:
sms_enabled = 'off'
# If it's a bandwidth number update the SMS enable status for the TN
if routing.provider == 'bandwidth':
unformatted_phone_number = routing.phonenumber
bw_pn_formatted = unformatted_phone_number.replace("+1", "")
if routing.type == 'mobile':
dashboard_client = bw_dashboard_client(partner_account.partnership_id, tn_type='mobile')
dashboard_client.phone_numbers_options.update(partner_account_id=str(partnership_account_id),
phonenumber=bw_pn_formatted,
sms_option=sms_enabled,
#sms_campaign_id=app.config.get('SMS_MOBILE_CAMPAIGN_ID'),
#sms_campaign_class=app.config.get('SMS_MOBILE_CAMPAIGN_CLASS')
)
else:
dashboard_client = bw_dashboard_client(partner_account.partnership_id)
dashboard_client.phone_numbers_options.update(partner_account_id=str(partnership_account_id),
phonenumber=bw_pn_formatted,
sms_option=sms_enabled)
return jsonify(routing.as_dict())
@phonenumbers.route('/api/inbound/routings/<int:id_>', methods=['DELETE'])
@login_required
@subscription_required
@role_required('admin', 'partner', 'sysadmin')
def delete_routing(id_):
routing = Phone.query.filter(Phone.id == id_).first()
if not routing:
return jsonify(success=True)
partnership_account_id = routing.partnership_account_id
viewing_partnership_account = current_user.is_viewing_partnership
# Check if being viewed by super partner
if viewing_partnership_account:
partnership_account_id = current_user.get_user_viewing_partnership_account_id
# import partnership information to get partnership id
from buyercall.blueprints.partnership.models import Partnership, PartnershipAccount
partner_account = PartnershipAccount.query \
.filter(PartnershipAccount.id == partnership_account_id).first()
partner = Partnership.query.filter(Partnership.id == partner_account.partnership_id).first()
# If debugging, do not delete the numbers
if not app.config.get('DEBUG'):
if routing.provider == 'bandwidth':
delete_bw(routing)
elif routing.provider == 'twilio':
delete_twilio(routing)
routing.is_deactivated = True
routing.deactivated_on = datetime.now()
db.session.commit()
if routing.type == 'mobile':
# Get the endpoint associated with this number
from buyercall.blueprints.mobile.models import Endpoint
endpoint = Endpoint.query.filter(Endpoint.inbound_id == id_).first()
# Lets get the domain now using the partnership_id
from ..mobile.models import Domain
if endpoint:
realm = Domain.query.filter(Domain.id == endpoint.domain_id).first()
# Deactivate the sip_endpoint in BuyerCall
Endpoint.deactivate(endpoint.id, current_user.partnership_account_id)
# Delete the endpoint from the provider
if routing.provider == 'bandwidth' and realm:
dashboard_client = bw_dashboard_client(partner.id, tn_type='mobile')
dashboard_client.realms.delete_sip_credentials(
realm_id=realm.domain_id,
endpoint_id=endpoint.provider_id,
)
return jsonify(success=True)
def delete_bw(routing):
# import partnership information to get partnership id
from ..partnership.models import Partnership, PartnershipAccount
partner_account = PartnershipAccount.query \
.filter(PartnershipAccount.id == routing.partnership_account_id).first()
partner = Partnership.query.filter(Partnership.id == partner_account.partnership_id).first()
if routing.type == 'mobile':
dashboard_client = bw_dashboard_client(partner.id, tn_type='mobile')
else:
dashboard_client = bw_dashboard_client(partner.id)
# Check to see if there's a phone number disconnect subscription available for the partnership
current_subscription = NumberSubscription.query \
.filter(NumberSubscription.order_type == 'disconnects').first()
# If there's no current subscription we need to create one and save it to the database
if current_subscription is None:
call_back_url = 'https://' + app.config.get('SERVER_DOMAIN') + url_for('phonenumbers.subscription_callback')
create_subscription = dashboard_client.subscriptions.subscribe(order_type='disconnects',
callback_url=call_back_url,
expiry='3153600000')
subscription_id = create_subscription.rsplit('/', 1)[-1]
# Save the new order subscription to the database
subscription = NumberSubscription(
cpaas_provider_name='bandwidth',
partnership_id=partner.id,
order_type='disconnects',
subscription_id=subscription_id,
callback_url=call_back_url
)
db.session.add(subscription)
db.session.commit()
log.info('A new subscription has been added for phone number disconnects')
# Delete the phone number from Dashboard platform using V2 API
unformatted_phone_number = routing.phonenumber
bw_pn_formatted = unformatted_phone_number.replace("+1", "")
dashboard_client.phone_numbers.disconnect(partner_account.name, bw_pn_formatted)
def delete_twilio(routing):
# import partnership information to get partnership id
from ..partnership.models import Partnership, PartnershipAccount
partner_account = PartnershipAccount.query \
.filter(PartnershipAccount.id == routing.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()
sub_twilio_client = subaccount_client(
current_user.subscription.twilio_subaccount_sid, partner.id)
sub_twilio_client.incoming_phone_numbers(routing.twilio_number_sid).delete()
@phonenumbers.route('/api/inbound/search', methods=['GET'])
@login_required
@subscription_required
@role_required('admin', 'partner', 'sysadmin')
def phonenumber_search():
""" Used for updating phone number search results. Arguments:
* tollfree - true if we're searching for a toll-free or local number
* prefix - local prefix (e.g. '312')
* phrase - a string of letters/numbers to search for, e.g. 'CARS'
* type - either tracking or priority or mobile.
"""
tollfree = (request.args.get('tollfree') == 'true')
type = request.args['type']
prefix = request.args.get('prefix')
phrase = request.args.get('phrase')
city = request.args.get('city')
state = request.args.get('state')
phrase = '*{}*'.format(phrase) if phrase else None
partnership_account_id = current_user.partnership_account_id
# What is the provider we're searching?
provider = 'bandwidth' if type in ['tracking', 'mobile'] else 'twilio'
try:
if provider == 'bandwidth':
numbers_list = bandwidth_search(tollfree, prefix, phrase, city, state, partnership_account_id, tn_type=type)
else:
numbers_list = twilio_search(tollfree, prefix, phrase, city, state, partnership_account_id)
return jsonify(data=numbers_list)
except ClientError as e:
log.error(traceback.format_exc())
return e.message, 400
except BandwidthException as e:
log.error(traceback.format_exc())
return e.message, 400
except TwilioRestException as e:
log.error(traceback.format_exc())
return e, 400
except Exception as e:
log.error(traceback.format_exc())
# error_details = e.message.split('DETAIL: ')
error_details = None
if error_details is not None and len(error_details) >= 2:
error_message = error_details[1]
else:
error_message = 'An unexpected error has occurred.'
return error_message, 400
def bandwidth_search(tollfree, prefix, phrase, city, state, partnership_account_id, tn_type=None):
# import partnership information to get partnership id
from ..partnership.models import Partnership, PartnershipAccount
partner_account = PartnershipAccount.query \
.filter(PartnershipAccount.id == partnership_account_id).first()
partner = Partnership.query.filter(Partnership.id == partner_account.partnership_id).first()
if tn_type and tn_type == 'mobile':
client = bw_dashboard_client(partner.id, tn_type='mobile')
else:
client = bw_dashboard_client(partner.id)
updated_number_list = []
if not tollfree:
kwargs = {}
count = 0
if prefix is not None and prefix is not '':
if len(prefix) != 3:
raise ClientError('An area code must be 3 digits.')
kwargs['areaCode'] = prefix
count = count + 1
if phrase is not None and phrase is not '':
kwargs['localVanity'] = phrase
count = count + 1
if city is not None and city is not '':
kwargs['city'] = city
count = count + 1
if state is not None and state is not '':
kwargs['state'] = state
count = count + 1
if count == 0:
raise ClientError('No search parameters provided.')
else:
kwargs['quantity'] = '18'
numbers = client.available_numbers.search(**kwargs)
# We converted the xml list of numbers to a dict, but now we need to load it as json object
no_json = json.loads(numbers)
# Then we need to work through the nested json to get just the list of numbers
if no_json['SearchResult'] is not None:
try:
numbers_list = no_json['SearchResult']['TelephoneNumberList']['TelephoneNumber']
updated_number_list = []
for number in numbers_list:
number = '+1' + number
updated_number_list.append(number)
log.info('the numbers are: {}'.format(updated_number_list))
except BandwidthException as e:
log.error('The exception is: {}'.format(e))
else:
# The prefix is the 3 digits of the Toll-free number. However, Bandwidth only allows 3 characters
# and you have to include a wildcard(*), therefore we remove a digit from prefix
kwargs = {}
if prefix is not None and prefix is not '':
pattern = (prefix[:-1] or '') + '*'
# Set the query string for the GET available search request for toll-free numbers
kwargs['tollFreeWildCardPattern'] = pattern
else:
pattern = (phrase or '') + '*'
kwargs['tollFreeVanity'] = pattern
# Return 12 numbers to choose from in the UI
kwargs['quantity'] = '18'
numbers = client.available_numbers.search(**kwargs)
log.info('the numbers are: {}'.format(numbers))
# We converted the xml list of numbers to a dict, but now we need to load it as json object
no_json = json.loads(numbers)
# Then we need to work through the nested json to get just the list of numbers
if no_json['SearchResult'] is not None:
try:
numbers_list = no_json['SearchResult']['TelephoneNumberList']['TelephoneNumber']
updated_number_list = []
for number in numbers_list:
number = '+1' + number
updated_number_list.append(number)
log.info('the numbers are: {}'.format(updated_number_list))
except BandwidthException as e:
log.error('The exception is: {}'.format(e))
return updated_number_list
def twilio_search(tollfree, prefix, phrase, city, state, partnership_account_id):
# import partnership information to get partnership id
from ..partnership.models import Partnership, PartnershipAccount
partner_account = PartnershipAccount.query \
.filter(PartnershipAccount.id == 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()
twilio_client = account_client(partner.id)
country = app.config.get('COUNTRY', 'US')
if not tollfree:
numbers = twilio_client.available_phone_numbers(country).local.list(
area_code=prefix,
contains=phrase,
in_region=state
)
else:
numbers = twilio_client.available_phone_numbers(country).toll_free.list(
area_code=prefix,
contains=phrase
)
numbers_list = [x.phone_number for x in numbers]
return numbers_list
@phonenumbers.route('/inbound/edit/<int:id>', methods=['GET', 'POST'])
@subscription_required
@role_required('admin')
@login_required
def phone_edit(id):
phone = Phone.query.get(id)
form = PhoneForm(obj=phone)
if form.validate_on_submit():
form.populate_obj(phone)
phone.save()
flash(_('The phone number has been updated successfully.'), 'success')
return redirect(url_for('phonenumbers.inbound_list'))
return render_template('phonenumbers/edit.jinja2', phone=phone,
form=form)
@phonenumbers.route('/inbound/phonenumberSubscriptionCallback/', methods=['GET', 'POST'])
@csrf.exempt
def subscription_callback():
# This function is a call back url after an phone number action, like a order is performed
# Get the params of the Request
data = xmltodict.parse(request.data)
json_data = json.dumps(data)
load_json_data = json.loads(json_data)
from .tasks import send_callback_email
send_callback_email.delay(load_json_data)
log.info('The phone number subscription callback content is: {}'.format(load_json_data))
return ''