HEX
Server: Apache/2.4.52 (Ubuntu)
System: Linux spn-python 5.15.0-89-generic #99-Ubuntu SMP Mon Oct 30 20:42:41 UTC 2023 x86_64
User: arjun (1000)
PHP: 8.1.2-1ubuntu2.20
Disabled: NONE
Upload Files
File: //home/arjun/projects/buyercall_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)