<commit_before>from twilio.base.exceptions import TwilioRestException
from twilio.rest import Client

from dimagi.utils.logging import notify_exception

from corehq import toggles
from corehq.apps.sms.models import SMS, PhoneLoadBalancingMixin, SQLSMSBackend
from corehq.messaging.smsbackends.twilio.forms import TwilioBackendForm

# https://www.twilio.com/docs/api/errors/reference
INVALID_TO_PHONE_NUMBER_ERROR_CODE = 21211
WHATSAPP_LIMITATION_ERROR_CODE = 63032

WHATSAPP_PREFIX = "whatsapp:"


class SQLTwilioBackend(SQLSMSBackend, PhoneLoadBalancingMixin):

    class Meta(object):
        app_label = 'sms'
        proxy = True

    @classmethod
    def get_available_extra_fields(cls):
        return [
            'account_sid',
            'auth_token',
        ]

    @classmethod
    def get_api_id(cls):
        return 'TWILIO'

    @classmethod
    def get_generic_name(cls):
        return "Twilio"

    @classmethod
    def get_form_class(cls):
        return TwilioBackendForm

    @classmethod
    def get_opt_in_keywords(cls):
        return ['START']

    @classmethod
    def get_pass_through_opt_in_keywords(cls):
        return ['YES']

    @classmethod
    def get_opt_out_keywords(cls):
        return ['STOP', 'STOPALL', 'UNSUBSCRIBE', 'CANCEL', 'END', 'QUIT']

    def _convert_to_whatsapp(self, number):
        if WHATSAPP_PREFIX not in number:
            number = f"{WHATSAPP_PREFIX}{number}"
        return number

    def _convert_from_whatsapp(self, number):
        if WHATSAPP_PREFIX in number:
            number = number.replace(WHATSAPP_PREFIX, "")
        return number

    def send(self, msg, orig_phone_number=None, *args, **kwargs):
        if not orig_phone_number:
            raise Exception("Expected orig_phone_number to be passed for all "
                            "instances of PhoneLoadBalancingMixin")

        if toggles.WHATSAPP_MESSAGING.enabled(msg.domain) and not kwargs.get('skip_whatsapp', False):
            orig_phone_number = self._convert_to_whatsapp(orig_phone_number)

        config = self.config
        client = Client(config.account_sid, config.auth_token)
        to = msg.phone_number
        from_ = orig_phone_number
        msg.system_phone_number = from_
        body = msg.text
        try:
            message = client.messages.create(
                body=body,
                to=to,
                from_=from_
            )
        except TwilioRestException as e:
            if e.code == INVALID_TO_PHONE_NUMBER_ERROR_CODE:
                msg.set_system_error(SMS.ERROR_INVALID_DESTINATION_NUMBER)
                return
            elif e.code == WHATSAPP_LIMITATION_ERROR_CODE:
                notify_exception(None, "Error with Twilio Whatsapp: %s" % str(e))
                orig_phone_number = self._convert_from_whatsapp(orig_phone_number)<commit_msg>Switched conditionals to asserts<commit_after>from twilio.base.exceptions import TwilioRestException
from twilio.rest import Client

from dimagi.utils.logging import notify_exception

from corehq import toggles
from corehq.apps.sms.models import SMS, PhoneLoadBalancingMixin, SQLSMSBackend
from corehq.messaging.smsbackends.twilio.forms import TwilioBackendForm

# https://www.twilio.com/docs/api/errors/reference
INVALID_TO_PHONE_NUMBER_ERROR_CODE = 21211
WHATSAPP_LIMITATION_ERROR_CODE = 63032

WHATSAPP_PREFIX = "whatsapp:"


class SQLTwilioBackend(SQLSMSBackend, PhoneLoadBalancingMixin):

    class Meta(object):
        app_label = 'sms'
        proxy = True

    @classmethod
    def get_available_extra_fields(cls):
        return [
            'account_sid',
            'auth_token',
        ]

    @classmethod
    def get_api_id(cls):
        return 'TWILIO'

    @classmethod
    def get_generic_name(cls):
        return "Twilio"

    @classmethod
    def get_form_class(cls):
        return TwilioBackendForm

    @classmethod
    def get_opt_in_keywords(cls):
        return ['START']

    @classmethod
    def get_pass_through_opt_in_keywords(cls):
        return ['YES']

    @classmethod
    def get_opt_out_keywords(cls):
        return ['STOP', 'STOPALL', 'UNSUBSCRIBE', 'CANCEL', 'END', 'QUIT']

    def _convert_to_whatsapp(self, number):
        assert WHATSAPP_PREFIX not in number, "Attempted to re-convert a number already formatted for Whatsapp"
        return f"{WHATSAPP_PREFIX}{number}"

    def _convert_from_whatsapp(self, number):
        assert WHATSAPP_PREFIX in number, "Attempted to convert a number that is not formatted for Whatsapp"
        return number.replace(WHATSAPP_PREFIX, "")

    def send(self, msg, orig_phone_number=None, *args, **kwargs):
        if not orig_phone_number:
            raise Exception("Expected orig_phone_number to be passed for all "
                            "instances of PhoneLoadBalancingMixin")

        if toggles.WHATSAPP_MESSAGING.enabled(msg.domain) and not kwargs.get('skip_whatsapp', False):
            orig_phone_number = self._convert_to_whatsapp(orig_phone_number)

        config = self.config
        client = Client(config.account_sid, config.auth_token)
        to = msg.phone_number
        from_ = orig_phone_number
        msg.system_phone_number = from_
        body = msg.text
        try:
            message = client.messages.create(
                body=body,
                to=to,
                from_=from_
            )
        except TwilioRestException as e:
            if e.code == INVALID_TO_PHONE_NUMBER_ERROR_CODE:
                msg.set_system_error(SMS.ERROR_INVALID_DESTINATION_NUMBER)
                return
            elif e.code == WHATSAPP_LIMITATION_ERROR_CODE:
                notify_exception(None, "Error with Twilio Whatsapp: %s" % str(e))
                orig_phone_number = self._convert_from_whatsapp(orig_phone_number)
{'fraction_non_alphanumeric': 0.07717541436464088, 'fraction_numerical': 0.003453038674033149, 'mean_word_length': 2.72540192926045, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 2, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3511422', 'n_tokens_mistral': 1825, 'n_tokens_neox': 1746, 'n_words': 366}