File: //home/arjun/projects/buyercall_new/buyercall/buyercall/blueprints/mobile/mobile_inbound_message.py
"""
Governs the logic of the incoming lead calls and lead/agent interations.
At any given moment the call is in one of the following states:
* 'NEW': New inbound call
* 'CALLBACK': Callback call, agent called first
* 'LEAD_ONHOLD': Lead selected a routing, placed on hold until agent answers
* 'AGENT_ONHOLD': In a scheduled callback, the agent is called first and placed
on hold.
* 'CALLBACK_PROMPT': Lead has been waiting for a while and is asked if he wants
to be called back.
* 'ONGOING': Agent answered/call ongoing.
* 'MISSED': Call ended/lead missed.
* 'CAPTURED': Call ended/lead captured.
"""
import logging
import traceback
import json
import os.path as path
import uuid
from flask import (
Blueprint,
request,
Response,
make_response,
jsonify,
current_app as app
)
from sqlalchemy import and_
import sys
import redis
from datetime import datetime, timedelta
import pytz
from urllib.request import urlopen
from urllib.error import HTTPError
from io import StringIO, BytesIO
from ..mobile.models import Endpoint
from urllib.parse import urlparse
import random
import string
from ..sms.models import Message
from ..contacts.models import Contact
from ..filters import format_phone_number
from ..block_numbers.models import Block
from PIL import Image, JpegImagePlugin, ExifTags
from buyercall.extensions import csrf, db
log = logging.getLogger(__name__)
mobile_message_inbound = Blueprint(
'mobile_message_inbound', __name__, template_folder='templates'
)
redis_db = redis.StrictRedis(host='redis')
provider = "bandwidth"
JpegImagePlugin._getmp = lambda x: None
# Testing Bandwidth Migration
@mobile_message_inbound.route('/bw/mobile/sms', methods=['GET', 'POST'])
@csrf.exempt
def inbound_mobile_sms():
""" Entry point for the incoming SMS or MMS messages. This is specifically for Bandwidth API V2. A Location
exist with 2 application attach to it. One is for SMS/MMS and the other for Voice. This call-back url
is specified on the SMS.MMS application.
"""
from buyercall.lib.util_bandwidth import authenticate_bw_request
authenticated = authenticate_bw_request(request.headers)
if authenticated:
# Fetch the request data. This includes message data for incoming sms/mms
args = request.json
mobile_inbound_message(args)
status_code = 201
message = jsonify(message='Authentication passed.')
response = make_response(message, status_code)
return response
else:
print('Authentication failed')
status_code = 401
message = jsonify(message='Authentication failed.')
response = make_response(message, status_code)
return response
def mobile_inbound_message(args):
# This function is a call back url for SIP incoming text messages.
# Whenever a text message is received by a SIP account this call back url will be called.
# Declare the args parameters to get sms/mms message data
message = args[0].get('message', '')
from_ = message.get('from', '')
to_ = args[0].get('to', '')
delivery_description = args[0].get('description', '')
text = message.get('text', '')
lowercase_text_body = text.lower()
# Determine if there's media attached to the message making it a MMS instead of a sms
media = message.get('media', '')
if to_:
# Determine the BuyerCall to establish the inbound id and partnership account
from ..phonenumbers.models import Phone
inbound = Phone.query.filter(Phone.phonenumber == to_).first()
partnership_account_id = inbound.partnership_account_id
else:
log.info('Something went wrong with receiving a sms/mms message. args: {}'.format(args))
return ''
# First check if the phone number sending the message is not blocked
is_number_blocked = Block.blocked(inbound.id, from_)
if is_number_blocked:
log.info('This phone number: {} was blocked and the message was not received'.format(from_))
return ''
# Add message to message table
from .tasks import add_mobile_message_lead
add_mobile_message_lead.delay(inbound.id, args, partnership_account_id, provider)
return ''
@mobile_message_inbound.route('/bw/mobile/sms/status/callback', methods=['GET', 'POST'])
@csrf.exempt
def mobile_sms_status_callback():
""" Entry point for the incoming SMS or MMS messages status callback. This is specifically for Bandwidth API
V2. A Location exist with 2 application attach to it. One is for SMS/MMS and the other for Voice. This call-back url
is specified on the SMS.MMS application.
"""
from buyercall.lib.util_bandwidth import authenticate_bw_request
authenticated = authenticate_bw_request(request.headers)
if authenticated:
# Fetch the request data. This includes message data for incoming sms/mms
args = request.json or request.args
print(args)
status_code = 201
message = args[0].get('message', '')
msg_id = message.get('id', '')
delivery_code = args[0].get('errorCode', 201)
delivery_description = args[0].get('description', '')
delivery_type = args[0].get('type', '')
from ..sms.bw_sms_tasks import update_msg_record
update_msg_record.apply_async(args=[msg_id, delivery_code, delivery_type, delivery_description], countdown=1)
message = jsonify(message='Authentication passed.')
response = make_response(message, status_code)
return response
else:
print('Authentication failed')
status_code = 401
message = jsonify(message='Authentication failed.')
response = make_response(message, status_code)
return response
@mobile_message_inbound.route('/mobile/send_message/', methods=['GET', 'POST'])
@csrf.exempt
def mobile_send_message():
# This function is a called by Acrobits when a message is sent from a SIP softphone
# Whenever a message is sent by a SIP account this call back url will be triggered
# Initiate Bandwidth API
# client = bw_client()
# Allowed file types to be sent via text messages. All files being sent and receive must be 1.5MB or smaller. GIF
# and VIDEO needs to be supported. Not working currently.
img_file_types = ['image/jpeg', 'image/png', 'image/bmp', 'image/gif', 'image/tiff', 'image/svg+xml']
# Get the params of the Request
args = request.json or request.args
log.info('The full list of fields receive from Acrobits for send message are: {}'.format(args))
# Retrieve all the data from the POST request sent by SIP device. This is for outbound messages
sip_username = args['from']
sip_password = args['password']
# Return the BuyerCall sip endpoint based on the sip info sent in the request
sip = Endpoint.query.filter(Endpoint.sip_username == sip_username, Endpoint.sip_password == sip_password).first()
to = args['to']
body = args['body']
content_type = args['content_type']
log.info('The content type is {}'.format(content_type))
print(body)
media = ''
media_text = ''
# Check to see if the to number is an existing contact and if so check if the
# contact unsubscribed in which case no message should be sent
if sip:
contact = Contact.query.filter(and_(Contact.phonenumber_1 == format_phone_number(to),
Contact.partnership_account_id == sip.partnership_account_id)).first()
log.info('The to number after formatting looks like: {}'.format(format_phone_number(to)))
if contact:
log.info('A message is being sent to contact id: {} '.format(contact.id))
if contact.is_unsubscribe:
log.info('The contact unsubscribed. The message was not sent to contact id: {}'.format(contact.id))
return ''
try:
j_b = json.loads(body)
try:
media_text_body = j_b['body']
log.info('The body of the media message is {}'.format(media_text_body))
media_text = media_text_body
except Exception as e:
log.info('There seems to be no body text attach to message sent to: {}'.format(to))
attachments = j_b['attachments']
media = []
for i in attachments:
log.info('The raw attachment info: {}'.format(i))
content_url = i["content-url"]
# content_url = 'https://s3.amazonaws.com/buyercall-test-mms-attachments/test/encrypted'
log.info('the content-url is: {}'.format(content_url))
file_content_type = i["content-type"]
log.info('The file content type is: {}'.format(file_content_type))
encrypt_key = i["encryption-key"]
# encrypt_key = 'F4EC56A83CDA65B2C6DC11E2CF693DAA'
log.info('the encryption key is: {}'.format(encrypt_key))
# media_file_name = i['filename']
# log.info('the media filename is: {}'.format(media_file_name))
# media_file_ext = path.splitext(media_file_name.lower())[1]
# log.info('The media file extension is: {}'.format(media_file_ext))
if file_content_type in img_file_types:
file_type = 'image'
else:
file_type = 'other'
encrypted_media_url = content_url
try:
r = urlopen(encrypted_media_url)
except HTTPError as e:
print(e.fp.read())
media_content = r
from buyercall.blueprints.mobile.views import decrypt_media
decrypted_file = decrypt_media(media_content, encrypt_key)
if file_type == 'image':
s_decrypt = BytesIO(decrypted_file)
foo = Image.open(s_decrypt)
if foo._getexif():
log.info('The image EXIF is {}'.format(foo._getexif().items()))
exif = dict((ExifTags.TAGS[k], v) for k, v in foo._getexif().items() if k in ExifTags.TAGS)
print(exif)
if exif['Orientation'] == 6:
foo = foo.rotate(-90, expand=True)
foo.thumbnail((550, 550), Image.ANTIALIAS)
log.info('The mms media attachment attributes are format: {}. mode: {} and size: {}'.
format(foo.format, foo.mode, foo.size))
filename = str(uuid.uuid4()) + '.jpeg'
file_path = path.join(app.config['UPLOAD_FOLDER'], filename)
foo.save(file_path, format='JPEG')
else:
filename = str(uuid.uuid4())
file_path = path.join(app.config['UPLOAD_FOLDER'], filename)
with open(file_path, 'w') as f:
f.write(decrypted_file)
from ..partnership.models import PartnershipAccount
partner_account = PartnershipAccount.query\
.filter(PartnershipAccount.id == sip.partnership_account_id).first()
partner_name = partner_account.name
from ..sms.tasks import upload_mms_image
media_url = upload_mms_image(
partner_name,
partner_account.id,
filename,
file_path
)
media.append(media_url)
log.info('The list of media urls are: {}'.format(media))
except Exception as e:
log.info('There is no attachment attach to message sent by {}. This is not an error, but a warning'.format(to))
try:
if sip:
from buyercall.blueprints.agents.models import Agent
agent_list = []
agent = Agent.query.filter(Agent.id == sip.agent_id).first()
if agent:
agent_list.append(agent.id)
# Send a mms using function in bw celery task
from ..sms.bw_sms_tasks import bw_send_sms
if media:
send_message = bw_send_sms(sip.inbound_id, to, media_text, agent_list, media)
else:
send_message = bw_send_sms(sip.inbound_id, to, body, agent_list)
log.info('The sent mms message id is {}'.format(send_message))
# When Acrobits sends a message we respond with the message id
r = {
"sms_id": send_message
}
json_data = json.dumps(r, default=str)
log.info('The response body sent to Acrobits when a text message is sent is: {}'.format(json_data))
return Response(response=json_data, status=200, mimetype='application/json; charset=utf-8')
else:
log.info('No SIP endpoint account exist for sip username {}'.format(sip_username))
except Exception as e:
log.error("Cannot sent message from SIP softphone: {}".format(traceback.format_exc()))
return ''
@mobile_message_inbound.route('/mobile/fetch_message/', methods=['GET', 'POST'])
@csrf.exempt
def mobile_fetch_message():
# This function is a called by Acrobits to check and fetch any new message
# send by the sip endpoint. This function gets called when a push notification is sent
# or when the user opens the app. This function should respond with the new
# message being sent as Request response.
# Get the params of the Request
args = request.json or request.args
print(args)
log.info('The information returned by the mobile fetch request is {}'.format(args))
# Retrieve all the data from the POST request sent by SIP device (Acrobits). This is for outbound messages
sip_username = args['username']
log.info('The username return by the fetch request is {}'.format(args['username']))
# sip_password = args['password']
last_sms_received_id = args['last_id']
last_sms_sent_id = args['last_sent_id']
log.info('The last sms sent id return by the fetch request is {}'.format(args['last_sent_id']))
# device = args['device']
endpoint = Endpoint.query.filter(Endpoint.sip_username == sip_username).first()
if endpoint is not None:
log.info('The endpoint info used in the fetch request function is {}'.format(endpoint))
endpoint.last_sms_received_id = last_sms_received_id
endpoint.last_sms_sent_id = last_sms_sent_id
db.session.commit()
inbound_messages = Message.query.filter(and_(Message.inbound_id == endpoint.inbound_id,
Message.direction == 'inbound')) \
.order_by(Message.id.desc()) \
.limit(50).all()
outbound_messages = Message.query.filter(and_(Message.inbound_id == endpoint.inbound_id,
Message.direction == 'outbound')) \
.order_by(Message.id.desc()) \
.limit(50).all()
last_inbound_message_date = datetime.now() - timedelta(days=90)
last_outbound_message_date = datetime.now() - timedelta(days=90)
if inbound_messages or outbound_messages:
# These list will because the JSON structure for unread and sent messages
if last_sms_received_id:
try:
last_inbound_message = Message.query.filter(and_(Message.inbound_id == endpoint.inbound_id,
Message.provider_message_id == last_sms_received_id))\
.first()
last_inbound_message_date = last_inbound_message.created_on
except Exception as e:
log.info('No inbound message was not found and default last inbound message '
'date is assigned for inbound id: {}'.format(endpoint.inbound_id))
if last_sms_sent_id:
try:
last_outbound_message = Message.query.filter(and_(Message.inbound_id == endpoint.inbound_id,
Message.provider_message_id == last_sms_sent_id))\
.first()
last_outbound_message_date = last_outbound_message.created_on
except Exception as e:
log.info('No outbound message was not found and default last outbound message '
'date is assigned for inbound id: {}'.format(endpoint.inbound_id))
unread_smss_list = []
sent_smss_list = []
utc = pytz.UTC
current_time = datetime.now().isoformat()
for in_m in inbound_messages:
if in_m.created_on > last_inbound_message_date.replace(tzinfo=utc):
sending_date = in_m.created_on.isoformat()
message_body = in_m.body_text
# We need to ignore messages that is used to unsubscribe or stop
# Receiving messages. These messages does not have to reach
# the mobile user
keyword_list = ['stop', ' stop', 'stop ', 'unsubscribe', ' unsubscribe',
'unsubscribe ', 'un-subscribe', 'unsubcribed']
message_body_lower = message_body.lower()
if message_body_lower in keyword_list:
if in_m.contact_id:
log.info('The message is an unsubscribe message from contact id: {}. This message'
'will not be sent to the mobile app'.format(in_m.contact_id))
else:
log.info('There is no contact attached to the message. This message is not sent to the'
'mobile app because it contains an unsubscribe keyword.')
else:
if in_m.type == 'mms':
content_type = "application/x-acro-filetransfer+json"
media_string = in_m.media_url.replace("{", "").replace("}", "")
media_split = list(media_string.split(","))
media_attachment_list = []
for i in media_split:
try:
img_path = urlparse(i).path
ext = path.splitext(img_path)[1]
if ext in ['jpg', 'jpeg', 'png', 'tiff', 'tif', 'bmp']:
media_file_name = ''.join(random.choices(
string.ascii_uppercase + string.digits, k=6)) + '.' + ext
else:
media_file_name = ''.join(random.choices(
string.ascii_uppercase + string.digits, k=6)) + '.jpg'
except:
media_file_name = i.rpartition('mms_')[2]
key_list = ["content-url", "filename"]
value_list = [i, media_file_name]
attachment_dict = {key: value for (key, value) in zip(key_list, value_list)}
media_attachment_list.append(attachment_dict)
message_body = {
"body": in_m.body_text,
"attachments": media_attachment_list
}
message_body = json.dumps(message_body)
else:
content_type = "text/plain"
unread_key_list = ["sms_id", "sending_date", "sender", "sms_text", "content_type"]
unread_value_list = [in_m.provider_message_id, sending_date, in_m.from_,
message_body, content_type]
unread_dicti = {key: value for (key, value) in zip(unread_key_list, unread_value_list)}
unread_smss_list.append(unread_dicti)
for out_m in outbound_messages:
if out_m.created_on > last_outbound_message_date.replace(tzinfo=utc):
sending_date = out_m.created_on.isoformat()
message_body = out_m.body_text
if out_m.type == 'mms':
content_type = "application/x-acro-filetransfer+json"
media_string = out_m.media_url.replace("{", "").replace("}", "")
media_split = list(media_string.split(","))
media_attachment_list = []
for i in media_split:
media_file_name = i.rpartition('mms_')[2]
key_list = ["content-url", "filename"]
value_list = [i, media_file_name]
attachment_dict = {key: value for (key, value) in zip(key_list, value_list)}
media_attachment_list.append(attachment_dict)
message_body = {
"body": out_m.body_text,
"attachments": media_attachment_list
}
message_body = json.dumps(message_body)
log.info('The normal message body is: {}'.format(message_body))
else:
content_type = "text/plain"
sent_key_list = ["sms_id", "sending_date", "recipient", "sms_text", "content_type"]
sent_value_list = [out_m.provider_message_id, sending_date, out_m.to, message_body, content_type]
sent_dicti = {key: value for (key, value) in zip(sent_key_list, sent_value_list)}
sent_smss_list.append(sent_dicti)
try:
d = {
"date": current_time,
"unread_smss": unread_smss_list,
"sent_smss": sent_smss_list
}
json_data = json.dumps(d, default=str)
log.info('The response body sent to Acrobits are: {}'.format(json_data))
return Response(response=json_data, status=200, mimetype='application/json; charset=utf-8')
except Exception as e:
log.debug('Something went from with the fetch message post heres the exception: ' + str(sys.exc_info()[0]))
else:
log.info('There is no inbound or outbound message to send to the fetch method for sip user: {}'.format(sip_username))
return ''
else:
log.info('No sip endpoint exist for username: {} to get device specific information'.format(sip_username))
return ''
@mobile_message_inbound.route('/mobile/token/', methods=['GET', 'POST'])
@csrf.exempt
def push_token_reporter():
# Every time a user logs onto a mobile app this function will be called
# This is important because the sip endpoint will be updated with a token that
# is required to identify the device when sending push notifications
args = request.json or request.args
log.info('The JSON response received from acrobits through the push token reporter is: {}'.format(args))
username = args.get('username')
endpoint = Endpoint.query.filter(Endpoint.sip_username == username).first()
if endpoint is not None:
endpoint.device_token = args.get('token')
endpoint.selector = args.get('selector')
endpoint.app_id = args.get('appId')
endpoint.install_id = args.get('installId')
endpoint.imei = args.get('imei')
endpoint.unique_id = args.get('uniqueId')
endpoint.build = args.get('build')
endpoint.platform = args.get('platform')
endpoint.platform_version = args.get('platformVersion')
endpoint.version = args.get('version')
endpoint.app_name = args.get('appName')
endpoint.locale = args.get('locale')
endpoint.cpu = args.get('cpu')
endpoint.device = args.get('device')
endpoint.production_build = args.get('productionBuild')
db.session.commit()
else:
log.info('No sip endpoint exist for username: {} to get device specific information'.format(username))
return ''
@mobile_message_inbound.route('/mobile/push/', methods=['GET', 'POST'])
@csrf.exempt
def send_push_notification():
args = request.json or request.args
log.info('The request info received: {}'.format(args))
p_type = args["push_type"]
username = args["sip_username"]
message = args["message"]
log.info('The message is {}'.format(message))
from buyercall.blueprints.mobile.tasks import push_notification
notification = push_notification(
sip_username=username,
push_type=p_type,
message=message)
return str(notification.status_code)