File: //home/arjun/projects/buyercall_forms/buyercall/buyercall/blueprints/api2/doc/endpoints/mobile.py
import logging
import traceback
from flask import jsonify
from flask import request
from flask_restx import Resource, reqparse
from sqlalchemy import and_
from buyercall.blueprints.api2.doc import serializers
from buyercall.blueprints.api2.doc.utils import api_validation
from buyercall.blueprints.api2.restplus import api
from buyercall.blueprints.mobile.models import Endpoint
from buyercall.blueprints.partnership.models import PartnershipAccount
from buyercall.blueprints.phonenumbers.models import Phone
from buyercall.blueprints.phonenumbers.views import purchase_bw_mobile, bandwidth_search, \
twilio_search, ClientError
from buyercall.extensions import db
from buyercall.lib.bandwidth import BandwidthException
from buyercall.lib.util_rest import rest_partnership, requires_auth, \
rest_is_partnership
log = logging.getLogger(__name__)
# ns = api.namespace('Mobile Phone Numbers', description='Operations related to mobile phone numbers.', path='/accounts')
validate = api_validation()
parser = reqparse.RequestParser()
parser.add_argument('city', type=str, location='args', help='The city')
parser.add_argument('state', type=str, location='args', help='The state')
parser.add_argument('code', type=str, location='args', help='The area code')
# @ns.route('/<int:paid>/mobile/availablenumbers')
@api.doc(responses={200: 'OK',
400: 'Error performing operation.',
401: 'Unauthorized request.'},
params={'paid': 'The partner account Id'})
class ApiMobileNumbersSearch(Resource):
@api.expect(parser)
@requires_auth
def get(self, paid):
"""
Retrieves the available phone numbers of a partnership account.
<p>
The Mobile Available Numbers API GET endpoint should be used to find all available phone numbers based on the
city and state. The type will always be required and it's recommended. This endpoint will return a list of
phone numbers that can be provisioned as mobile phone number that gets assigned to a mobile app.
</p>
<br />
<p>
Please note we can not guarantee that a specific phone number is available. If no results are returned for a
specific city and state revert to only search per State, which will return a broader search result.
</p>
<br />
<p>
You will require a partner authentication token, a partner account id as well as city and state
search parameters to make a successful request. A response will
be returned, similar to the example below, based
on a successful request:
<br />
<br />
</p>
<pre class="code-background" style="color: white">
{
"available_numbers": [
"+12246032407",
"+12246032408",
"+12246032430",
"+12246032435",
"+12246032436",
"+12246032447",
"+12246032480",
"+12246032493",
"+12246032495",
"+12246032521"
]
}
</pre>
"""
if rest_is_partnership and rest_partnership is not None:
error_message = 'An error occurred performing the search. '
partnership_account = PartnershipAccount \
.query \
.filter(and_(PartnershipAccount.id == paid, PartnershipAccount.partnership_id == rest_partnership.id)) \
.first()
if partnership_account is not None:
tollfree = False
type = 'mobile'
# phrase = request.args.get('phrase')
# phrase = '*{}*'.format(phrase) if phrase else None
phrase = None
prefix = None
args = parser.parse_args()
city = args['city']
state = args['state']
area_code = args['code']
if city is None:
city = ''
if state is None:
state = ''
if area_code is None:
area_code = ''
# 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, area_code, phrase, city, state,
partnership_account.id, tn_type=type)
else:
numbers_list = twilio_search(tollfree, area_code, phrase, city, state, partnership_account.id)
return numbers_list
except BandwidthException as e:
log.error(traceback.format_exc())
error_array = e.message.split('DETAIL: ')
if len(error_array) >= 2:
error_message = error_message + error_array[1]
return api.abort(400, error_message)
except ClientError as e:
log.error(traceback.format_exc())
error_message = "{} {}".format(error_message, e.message)
return api.abort(400, error_message)
except Exception as e:
log.error(traceback.format_exc())
error_details = e.message.split('DETAIL: ')
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 api.abort(400, error_message)
else:
return api.abort(code=400, message="Partnership account not found.")
else:
api.abort(401)
# @ns.route('/<int:paid>/mobile')
@api.doc(responses={200: 'OK',
400: 'Error performing operation.',
401: 'Unauthorized request.'},
params={'paid': 'The partner account Id'})
class ApiMobileNumbers(Resource):
@api.expect(serializers.mobile_add, validate=True)
@api.response(204, 'Phone number successfully created.')
@requires_auth
def post(self, paid):
"""
Provision phone numbers for a partnership account.
<p>
The Mobile Phone Number API POST endpoint should be used to provision a mobile number. During the provisioning
of the mobile number a mobile app user (also known as a sip endpoint) gets created based on the sip information
and agent provided. The mobile app will directly be associated to a mobile phone number and agent.
</p>
<br />
<p>
It recommended that the agent's phone number be changed after provisioning a mobile phone number associated with
a specific agent id. This can be achieved through the Agents PUT request endpoint.
</p>
<br />
<p>
It's also important to mention that you need to perform an available phone number lookup using the Mobile
Available Phone Number GET request endpoint before provisioning a number. Once you've found an available
phone number, you will be required to passed the available phone number through the phonenumber field during
this POST request to actually provision the phone number.
</p>
<br />
<p>
You require a partner authentication token, a partner account id and an available phone number to
make a successful request. A response will
be returned, similar to the example below, based
on a successful request:
<br />
<br />
</p>
<pre class="code-background" style="color: white">
{
"mobile_phonenumber_id": 72,
"partnership_account_id": 1,
"phonenumber": "+18722217507",
"qr_url": "https://qr_code_image_url.com"
}
</pre>
"""
if rest_is_partnership and rest_partnership is not None:
error_message = 'An error occurred provisioning number. '
qr_url_response = ''
username_exists = False
partnership_account = PartnershipAccount \
.query \
.filter(and_(PartnershipAccount.id == paid, PartnershipAccount.partnership_id == rest_partnership.id)) \
.first()
if partnership_account is not None:
received = request.json
routing = Phone(
partnership_account_id=partnership_account.id
)
if validate.phone_number(received['phonenumber']):
routing.phonenumber = received['phonenumber']
routing.friendly_name = received['friendly_name']
routing.routing_config = received['routing_config']
routing.routing_config['routingType'] = 'default'
routing.routing_config['language'] = 'en'
routing.type = 'mobile'
routing.local = True
routing.tollfree = False
routing.source = 'mobile'
routing.channel = None
routing.provider = 'bandwidth'
result_sip_description = None
result_sip_username = received['sip_username']
result_sip_password = received['sip_password']
if 'sip_description' in received:
result_sip_description = received['sip_description']
try:
log.info('Purchasing number from {}'.format(routing.provider))
username_exists = Endpoint.api_username_check(result_sip_username)
if not username_exists:
purchase_bw_mobile(routing, result_sip_username, result_sip_password, True,
result_sip_description, partnership_account)
routing.connect_audio_files()
sip_endpoint = Endpoint.query \
.filter(and_(Endpoint.sip_username == result_sip_username,
Endpoint.partnership_account_id == paid)) \
.first()
if sip_endpoint is not None:
if sip_endpoint.qr_url is not None:
qr_url_response = sip_endpoint.qr_url
return jsonify(
partnership_account_id=partnership_account.id,
mobile_phonenumber_id=routing.id,
phonenumber=routing.phonenumber,
qr_url=qr_url_response)
except BandwidthException as e:
log.error(traceback.format_exc())
error_array = e.message.split('DETAIL: ')
if len(error_array) >= 2:
error_message = "{} {}".format(error_message, error_array[1])
return api.abort(400, error_message)
except Exception as e:
log.error(traceback.format_exc())
db.session.rollback()
error_details = e.message.split('DETAIL: ')
if error_details is not None and len(error_details) >= 2:
error_message = error_details[1]
else:
error_message = 'Error provisioning mobile phone number.'
return api.abort(code=400, message=error_message)
if username_exists:
log.error("Error provisioning mobile phone number. Endpoint: {} already in use."
.format(result_sip_username))
return api.abort(code=400,
message="Error provisioning mobile phone number. Endpoint {} already in use.".
format(result_sip_username)
)
else:
return api.abort(code=400, message="Invalid mobile number provided. Number: {}".
format(received['phonenumber']))
else:
return api.abort(code=400, message="Partnership account not found.")
else:
api.abort(401)
# @ns.route('/<int:paid>/mobile/<int:pnid>')
@api.doc(responses={200: 'OK',
400: 'Error performing operation.',
401: 'Unauthorized request.'},
params={'paid': 'The partner account Id', 'pnid': 'The phone number Id'})
class ApiMobileNumbers(Resource):
@requires_auth
def get(self, paid, pnid):
"""
Retrieves a phone number of a partnership account.
<p>
The Mobile Phone Number GET endpoint should be used to return all information on a specific
mobile phone number for a partner account.
</p>
<br />
<p>
You require a partner authentication token and a partner account id as well as a phone number id to make a
successful request. A response will
be returned, similar to the example below, based
on a successful request:
<br />
<br />
</p>
<pre class="code-background" style="color: white">
{
"deactivated_on": "",
"friendly_name": "Phone number for app user goonryn",
"greetingMessage": false,
"id": 72,
"is_deactivated": false,
"phonenumber": "+18722217507",
"qr_url": "https://qr-code.com/image",
"routing_config": {
"defaultRouting": {
"agents": [
{
"contactUsing": "phone",
"fullname": "",
"id": 2
}
]
},
"recordCalls": false,
"voicemail": false,
"voicemailMessage": "Please leave a message after the beep.",
"voicemailMessageType": "text",
"whisperMessage": "This call is being recorded.",
"whisperMessageType": "text",
"transcribeAnsweredCall": false,
"transcribeVoiceMail": false
},
"sip_description": "mobile app for goonryn",
"sip_enabled": true,
"sip_password": "xfg34DFsde453SS=df#",
"sip_username": "goonryn"
}
</pre>
"""
if rest_is_partnership and rest_partnership is not None:
partnership_account = PartnershipAccount \
.query \
.filter(and_(PartnershipAccount.id == paid,
PartnershipAccount.partnership_id == rest_partnership.id)) \
.first()
if partnership_account is not None:
phone = db.session \
.query(Phone, Endpoint) \
.with_entities(Phone.id, Phone.phonenumber, Phone.routing_config, Phone.is_deactivated,
Phone.deactivated_on, Phone.friendly_name, Endpoint.sip_username,
Endpoint.sip_password, Endpoint.description, Endpoint.enabled, Endpoint.qr_url) \
.filter(and_(Phone.id == pnid, Phone.partnership_account_id == paid)) \
.filter(and_(Endpoint.inbound_id == pnid, Endpoint.partnership_account_id == paid)) \
.first()
if phone is not None:
try:
agent_list = []
defaultRouting = phone.routing_config['defaultRouting']
agents = defaultRouting['agents']
result_deactivated_on = ''
if phone.is_deactivated:
if phone.deactivated_on is not None:
result_deactivated_on = phone.deactivated_on.strftime('%Y-%m-%d %H:%M:%S')
for agent in agents:
contactAgentUsing = 'none'
if 'contactUsing' in agent:
contactAgentUsing = agent['contactUsing']
add_agent = {
'fullname': agent['fullName'],
'id': agent['id'],
'contactUsing': contactAgentUsing
}
agent_list.append(add_agent)
result_default_routing = {
'agents': agent_list
}
result_greeting_message = False
if 'greetingMessage' in phone.routing_config and phone.routing_config[
'greetingMessage'] is not None:
result_greeting_message = phone.routing_config['greetingMessage']
# Set the transcribe answered call boolean if it's not present in JSON routing. This is to cover
# old numbers with old settings
if 'transcribeAnsweredCall' in phone.routing_config and \
phone.routing_config['transcribeAnsweredCall'] is not None:
result_transcribe_answered_call = phone.routing_config['transcribeAnsweredCall']
else:
result_transcribe_answered_call = False
# Set the transcribe voicemail boolean if it's not present in JSON routing. This is to cover
# old numbers with old settings
if 'transcribeVoiceMail' in phone.routing_config and \
phone.routing_config['transcribeVoiceMail'] is not None:
result_transcribe_voicemail = phone.routing_config['transcribeVoiceMail']
else:
result_transcribe_voicemail = False
result_routing_config = {
'defaultRouting': result_default_routing,
# 'SMSAutoReply': phone.routing_config['SMSAutoReply'],
# 'SMSAutoReplyImage': phone.routing_config['SMSAutoReplyImage'],
# 'SMSAutoReplyImageUrl': phone.routing_config['SMSAutoReplyImageUrl'],
# 'SMSAutoReplyText': phone.routing_config['SMSAutoReplyText'],
# 'configSMSSetup': phone.routing_config['configSMSSetup'],
'greetingMessage': result_greeting_message,
'whisperMessageType': phone.routing_config['whisperMessageType'],
'whisperMessage': phone.routing_config['whisperMessage'],
# 'language': phone.routing_config['language'],
'recordCalls': phone.routing_config['recordCalls'],
# 'routingType': phone.routing_config['routingType'],
'voicemail': phone.routing_config['voicemail'],
'voicemailMessage': phone.routing_config['voicemailMessage'],
'transcribeAnsweredCall': result_transcribe_answered_call,
'transcribeVoiceMail': result_transcribe_voicemail
}
return jsonify(
id=phone.id,
phonenumber=phone.phonenumber,
friendly_name=phone.friendly_name,
routing_config=result_routing_config,
is_deactivated=phone.is_deactivated,
deactivated_on=result_deactivated_on,
sip_username=phone.sip_username,
sip_password=phone.sip_password,
sip_description=phone.description,
sip_enabled=phone.enabled,
qr_url=phone.qr_url
)
except Exception as e:
log.error(traceback.format_exc())
db.session.rollback()
return api.abort(code=500, message="Error retrieving phone number.")
else:
return api.abort(code=404, message="Phone number not found.")
else:
return api.abort(code=400, message="Partnership account not found.")
else:
api.abort(401)
@api.expect(serializers.mobile_edit, validate=True)
@api.response(200, 'Phone number successfully updated.')
@requires_auth
def put(self, paid, pnid):
"""
Update a phone number of a partnership account.
<p>
The Mobile Phone Numbers PUT endpoint should be used to update an Mobile Phone Number, including mobile app
user information (Also known as sip endpoint information) for a
partner account. Please note you can't change the actual phone number or mobile app username (sip username)
when performing a PUT request. You can only update the configuration and settings for the phone number.
</p>
<br />
<p>
You will require a partner authentication token, a partner account id as well as a phone number id to make a
successful request.
</p>
"""
if rest_is_partnership and rest_partnership is not None:
qr_url_response = ''
phone_edit_result = False
endpoint_edit_result = False
partnership_account = PartnershipAccount \
.query \
.filter(and_(PartnershipAccount.id == paid, PartnershipAccount.partnership_id == rest_partnership.id)) \
.first()
if partnership_account is not None:
received = request.json
result_recordCalls = None
result_voicemail = None
result_voicemail_message = None
result_voicemail_message_type = None
result_greeting_message = None
result_whisper_message = None
result_whisper_message_type = None
result_transcribe_answered_call = None
result_transcribe_voicemail = None
result_default_config = None
result_agent_full_name = None
result_agent_contact_using = None
result_agent_id = None
result_friendly_name = None
result_routing_config = None
result_sip_password = None
result_sip_description = None
result_sip_enabled = None
if 'friendly_name' in received:
result_friendly_name = received['friendly_name']
if 'routing_config' in received:
result_routing_config = received['routing_config']
if 'recordCalls' in result_routing_config:
result_recordCalls = result_routing_config['recordCalls']
if 'voicemail' in result_routing_config:
result_voicemail = result_routing_config['voicemail']
if 'voicemailMessage' in result_routing_config:
result_voicemail_message = result_routing_config['voicemailMessage']
if 'voicemailMessageType' in result_routing_config:
result_voicemail_message_type = result_routing_config['voicemailMessageType']
if 'greetingMessage' in result_routing_config:
result_greeting_message = result_routing_config['greetingMessage']
if 'whisperMessage' in result_routing_config:
result_whisper_message = result_routing_config['whisperMessage']
if 'whisperMessageType' in result_routing_config:
result_whisper_message_type = result_routing_config['whisperMessageType']
if 'defaultRouting' in result_routing_config:
result_default_config = result_routing_config['defaultRouting']
if 'agents' in result_default_config:
result_agents = result_default_config['agents']
if result_agents is not None:
for agent in result_agents:
if 'contactUsing' in agent:
result_agent_contact_using = agent['contactUsing']
if 'id' in agent:
result_agent_id = int(agent['id'])
if 'fullName' in agent:
result_agent_full_name = agent['fullName']
if 'transcribeAnsweredCall' in result_routing_config:
result_transcribe_answered_call = result_routing_config['transcribeAnsweredCall']
if 'transcribeVoiceMail' in result_routing_config:
result_transcribe_voicemail = result_routing_config['transcribeVoiceMail']
if 'sip_password' in received:
result_sip_password = received['sip_password']
if 'sip_description' in received:
result_sip_description = received['sip_description']
if 'sip_enabled' in received:
result_sip_enabled = received['sip_enabled']
endpoint = Endpoint.query \
.filter(and_(Endpoint.inbound_id == pnid,
Endpoint.partnership_account_id == paid)) \
.first()
phonenumber = Phone.query \
.filter(and_(Phone.partnership_account_id == paid,
Phone.id == pnid)) \
.first()
if endpoint is None and phonenumber is None:
return api.abort(code=400,
message="Error updating phone number. Username and phonenumber not found.")
elif endpoint is None:
return api.abort(code=400, message="Error updating phone number. Username not found.")
elif phonenumber is None:
return api.abort(code=400, message="Error updating phone number. Phonenumber not found.")
elif endpoint is not None and phonenumber is not None:
if not endpoint.is_deactivated and not phonenumber.is_deactivated:
try:
endpoint_edit_result = Endpoint.api_update(endpoint.id,
paid,
result_sip_password,
result_sip_description,
result_sip_enabled,
result_agent_id)
phone_edit_result = Phone.api_mobile_update(pnid,
paid,
result_friendly_name,
result_recordCalls,
result_voicemail,
result_voicemail_message,
result_voicemail_message_type,
result_greeting_message,
result_whisper_message,
result_whisper_message_type,
result_agent_id,
result_agent_full_name,
result_agent_contact_using,
result_transcribe_answered_call,
result_transcribe_voicemail)
except Exception as e:
log.error('Error updating phone number. Error: ' + str(e))
return api.abort(code=400, message="Error updating phone number.")
if phone_edit_result and endpoint_edit_result:
endpoint_result = Endpoint.query \
.filter(and_(Endpoint.inbound_id == pnid, Endpoint.partnership_account_id == paid)) \
.first()
phone_result = Phone.query \
.filter(and_(Phone.id == pnid, Phone.partnership_account_id == paid)) \
.first()
if endpoint_result is not None:
if endpoint_result.qr_url is not None:
qr_url_response = endpoint_result.qr_url
return jsonify(
partnership_account_id=paid,
mobile_phonenumber_id=pnid,
phonenumber=phone_result.phonenumber,
qr_url=qr_url_response)
else:
return api.abort(code=400, message="Error updating phone number.")
else:
return api.abort(code=400,
message="Error updating phone number. Username/phonenumber has been released.")
else:
return api.abort(code=400, message="Error updating phone number. Endpoint not found.")
else:
return api.abort(code=400, message="Partnership account not found.")
else:
return api.abort(401)
@api.response(204, 'Phone number successfully deleted.')
@requires_auth
def delete(self, paid, pnid):
"""
Delete a phone number of a partnership account.
<p>
The Mobile Phone Numbers DELETE endpoint should be used to deactivate an Mobile Phone Number for a
partner account. When a phone number is deactivated the actual phone number has been released to be provisioned
by someone else. However, we do not delete the phone number record for legacy purposes and the deactivated
phone number will still appear in the Mobile Phone Number GET request results.
</p>
<br />
<p>
You will require a partner authentication token, a partner account id as well as a phone number id to make a
successful request.
</p>
"""
if rest_is_partnership and rest_partnership is not None:
partnership_account = PartnershipAccount \
.query \
.filter(and_(PartnershipAccount.id == paid, PartnershipAccount.partnership_id == rest_partnership.id)) \
.first()
if partnership_account is not None:
try:
result = Phone.api_delete(pnid, paid)
if result:
return True, 204
else:
return api.abort(code=400, message="Error deleting phone number.")
except Exception as e:
log.error('Error deleting phone number. Error: ' + str(e))
return api.abort(code=400, message="Error deleting phone number.")
else:
return api.abort(code=400, message="Partnership account not found.")
else:
return api.abort(401)
# @ns.route('/<int:paid>/mobile/<int:pnid>/usage')
@api.doc(responses={200: 'OK',
400: 'Error performing operation.',
401: 'Unauthorized request.',
404: 'Phone number not found.',
},
params={'paid': 'The partner account Id', 'pnid': 'The phone number Id'})
class ApiMobilelPhoneNumbersUsage(Resource):
@requires_auth
def get(self, paid, pnid):
"""
Get the call and message usage of a number.
<p>
The Mobile Phone Number Usage API GET endpoint should be used to retrieve the usage data for a specific
mobile phone number. The usage data will include inbound calls, inbound messages, outbound calls and
outbound messages.
</p>
<br />
<p>
You require a partner authentication token, a partner account id and an available phone number to
make a successful request. A response will
be returned, similar to the example below, based
on a successful request:
<br />
<br />
</p>
<pre class="code-background" style="color: white">
{
"inbound_call_count": 0,
"inbound_message_count": 0,
"outbound_call_count": 0,
"outbound_message_count": 0
}
</pre>
"""
if rest_is_partnership and rest_partnership is not None:
partnership_account = PartnershipAccount \
.query \
.filter(and_(PartnershipAccount.id == paid, PartnershipAccount.partnership_id == rest_partnership.id)) \
.first()
if partnership_account is not None:
phone = Phone.query \
.filter(and_(Phone.id == pnid, Phone.partnership_account_id == paid)) \
.filter(Phone.type == 'mobile') \
.first()
if phone is not None:
try:
return partnership_account.phone_number_usage(paid, pnid)
except Exception as e:
log.error(traceback.format_exc())
return api.abort(code=500, message="Error retrieving phone number.")
else:
return api.abort(code=404,
message="Phone number not found. Are your sure this is a mobile number?")
else:
return api.abort(code=400, message="Partnership account not found.")
else:
api.abort(401)
# @ns.route('/<int:paid>/mobile/pushNotifications')
@api.doc(responses={200: 'OK',
400: 'Error performing operation.',
401: 'Unauthorized request.'},
params={'paid': 'The partner account Id'})
class ApiMobileNumberPushNotifications(Resource):
@api.expect(serializers.mobile_push_notification, validate=True)
@api.response(200, 'Notification successfully pushed.')
@requires_auth
def post(self, paid):
"""
Send a push notification to a mobile number.
<p>
The Mobile Push Notification API POST endpoint should be used to send ad hoc push notification messages to a
specific mobile app user by using their username.
</p>
<br />
<p>
The push notification endpoint can be used to send mobile phone number users personal messages. A message can be
a call for action or just a reminder. A message can be sent to the user. The push notification will open the
mobile app when clicked by the app user.
</p>
<br />
<p>
You require a partner authentication token, the message as well the username of the user that needs to receive
the push notification.
make a successful request. A response code will
be returned based on a successful request.
<p />
<br />
"""
if rest_is_partnership and rest_partnership is not None:
error_message = 'An error occurred pushing notification.'
partnership_account = PartnershipAccount \
.query \
.filter(and_(PartnershipAccount.id == paid, PartnershipAccount.partnership_id == rest_partnership.id)) \
.first()
if partnership_account is not None:
received = request.json
result_push_type = 'NotifyGenericTextMessage'
result_sip_username = received['sip_username']
result_message = received['message']
try:
sip_endpoint = Endpoint.query.filter(
and_(Endpoint.sip_username == result_sip_username, Endpoint.partnership_account_id == paid)
).first()
if sip_endpoint is not None:
from buyercall.blueprints.mobile.tasks import push_notification
r = push_notification(sip_username=result_sip_username,
push_type=result_push_type,
message=result_message)
return True, 200
else:
return api.abort(code=400, message="SIP endpoint not found.")
except Exception:
log.error(traceback.format_exc())
return api.abort(code=500, message="Error pushing notification.")
else:
return api.abort(code=400, message="Partnership account not found.")
else:
api.abort(401)