Ejemplo n.º 1
0
    def refresh_access_token(self, channel_id):
        r = get_redis_connection()
        lock_name = self.TOKEN_REFRESH_LOCK % self.channel_uuid

        if not r.get(lock_name):
            with r.lock(lock_name, timeout=30):
                key = self.TOKEN_STORE_KEY % self.channel_uuid

                post_data = dict(grant_type="client_credentials", client_id=self.app_id, client_secret=self.app_secret)
                url = self.TOKEN_URL

                event = HttpEvent("POST", url, json.dumps(post_data))
                start = time.time()

                response = self._request(url, post_data, access_token=None)
                event.status_code = response.status_code

                if response.status_code != 200:
                    event.response_body = response.content
                    ChannelLog.log_channel_request(
                        channel_id, "Got non-200 response from %s" % self.API_NAME, event, start, True
                    )
                    return

                response_json = response.json()
                event.response_body = json.dumps(response_json)
                ChannelLog.log_channel_request(
                    channel_id, "Successfully fetched access token from %s" % self.API_NAME, event, start
                )

                access_token = response_json["access_token"]
                expires = response_json.get("expires_in", 7200)
                r.set(key, access_token, ex=int(expires))
                return access_token
Ejemplo n.º 2
0
    def refresh_access_token(self, channel_id):
        r = get_redis_connection()
        lock_name = self.TOKEN_REFRESH_LOCK % self.channel_uuid

        if not r.get(lock_name):
            with r.lock(lock_name, timeout=30):
                key = self.TOKEN_STORE_KEY % self.channel_uuid

                post_data = dict(grant_type="client_credentials", client_id=self.app_id, client_secret=self.app_secret)
                url = self.TOKEN_URL

                event = HttpEvent("POST", url, json.dumps(post_data))
                start = time.time()

                response = self._request(url, post_data, access_token=None)
                event.status_code = response.status_code

                if response.status_code != 200:
                    event.response_body = response.content
                    ChannelLog.log_channel_request(
                        channel_id, "Got non-200 response from %s" % self.API_NAME, event, start, True
                    )
                    return

                response_json = response.json()
                event.response_body = json.dumps(response_json)
                ChannelLog.log_channel_request(
                    channel_id, "Successfully fetched access token from %s" % self.API_NAME, event, start
                )

                access_token = response_json["access_token"]
                expires = response_json.get("expires_in", 7200)
                r.set(key, access_token, ex=int(expires))
                return access_token
Ejemplo n.º 3
0
def check_calls_task():
    from .models import IVRCall

    now = timezone.now()

    calls_to_retry = (
        IVRCall.objects.filter(next_attempt__lte=now, retry_count__lte=IVRCall.MAX_RETRY_ATTEMPTS)
        .filter(status__in=IVRCall.RETRY_CALL)
        .filter(modified_on__gt=now - timedelta(days=IVRCall.IGNORE_PENDING_CALLS_OLDER_THAN_DAYS))
        .filter(direction=IVRCall.OUTGOING, is_active=True)
    )

    for call in calls_to_retry:

        ChannelLog.log_ivr_interaction(call, "Retrying call", HttpEvent(method="INTERNAL", url=None))

        call.status = IVRCall.PENDING
        call.next_attempt = None
        # reset the call
        call.started_on = None
        call.ended_on = None
        call.duration = 0
        call.modified_on = timezone.now()
        call.save(update_fields=("status", "next_attempt", "started_on", "ended_on", "duration", "modified_on"))

    if calls_to_retry:
        task_enqueue_call_events.apply_async()
Ejemplo n.º 4
0
def check_calls_task():
    from .models import IVRCall

    now = timezone.now()

    calls_to_retry = (IVRCall.objects.filter(
        next_attempt__lte=now,
        retry_count__lte=IVRCall.MAX_RETRY_ATTEMPTS).filter(
            status__in=IVRCall.RETRY_CALL).filter(
                modified_on__gt=now - timedelta(
                    days=IVRCall.IGNORE_PENDING_CALLS_OLDER_THAN_DAYS)).filter(
                        direction=IVRCall.OUTGOING))

    for call in calls_to_retry:

        ChannelLog.log_ivr_interaction(call, "Retrying call",
                                       HttpEvent(method="INTERNAL", url=None))

        call.status = IVRCall.PENDING
        call.next_attempt = None
        # reset the call
        call.started_on = None
        call.ended_on = None
        call.duration = 0
        call.modified_on = timezone.now()
        call.save(update_fields=("status", "next_attempt", "started_on",
                                 "ended_on", "duration", "modified_on"))

    if calls_to_retry:
        task_enqueue_call_events.apply_async()
Ejemplo n.º 5
0
def check_failed_calls_task():
    from .models import IVRCall

    # calls that have failed and have a `error_count` value are going to be retried
    failed_calls_to_retry = (IVRCall.objects.filter(
        error_count__gte=1, error_count__lte=IVRCall.MAX_ERROR_COUNT).filter(
            status__in=IVRCall.FAILED).filter(
                modified_on__gt=timezone.now() - timedelta(
                    days=IVRCall.IGNORE_PENDING_CALLS_OLDER_THAN_DAYS)).filter(
                        direction=IVRCall.OUTGOING))

    for call in failed_calls_to_retry:

        ChannelLog.log_ivr_interaction(call, "Retrying failed call",
                                       HttpEvent(method="INTERNAL", url=None))

        call.status = IVRCall.PENDING
        # reset the call
        call.started_on = None
        call.ended_on = None
        call.duration = 0
        call.modified_on = timezone.now()
        call.save(update_fields=("status", "next_attempt", "started_on",
                                 "ended_on", "duration", "modified_on"))

    if failed_calls_to_retry:
        task_enqueue_call_events.apply_async()
Ejemplo n.º 6
0
def check_calls_task():
    from .models import IVRCall

    now = timezone.now()

    calls_to_retry = (
        IVRCall.objects.filter(next_attempt__lte=now, retry_count__lte=IVRCall.MAX_RETRY_ATTEMPTS)
        .filter(status__in=IVRCall.RETRY_CALL)
        .filter(direction=IVRCall.OUTGOING, is_active=True)
    )

    for call in calls_to_retry:

        ChannelLog.log_ivr_interaction(call, "Retrying call", HttpEvent(method="INTERNAL", url=None))

        call.status = IVRCall.QUEUED
        call.next_attempt = None
        # reset the call
        call.started_on = None
        call.ended_on = None
        call.duration = 0
        call.modified_on = timezone.now()
        call.save()

        start_call_task.apply_async(kwargs={"call_pk": call.id}, queue="handler")
Ejemplo n.º 7
0
    def handle_direct_inbound(self, request, uuid, data):
        from warapidpro.types import WhatsAppDirectType
        channel = self.lookup_channel(WhatsAppDirectType.code, uuid)
        if not channel:
            error_msg = "Channel not found for id: %s" % (uuid, )
            logger.error(error_msg)
            return HttpResponse(error_msg, status=400)

        from_addr = data['from_addr']
        content = self.get_content(data)
        attachments = self.get_attachments(data)

        message = Msg.create_incoming(channel,
                                      URN.from_tel(from_addr),
                                      content,
                                      external_id=data['uuid'],
                                      attachments=attachments)
        response_body = {
            'message_id': message.pk,
        }

        request_body = request.body
        request_method = request.method
        request_path = request.get_full_path()

        event = HttpEvent(request_method, request_path, request_body, 201,
                          json.dumps(response_body))
        ChannelLog.log_message(message, 'Handled inbound message.', event)
        return JsonResponse(response_body, status=201)
Ejemplo n.º 8
0
    def refresh_access_token(self, channel_id):
        r = get_redis_connection()
        lock_name = JIOCHAT_ACCESS_TOKEN_REFRESH_LOCK % self.channel_uuid

        if not r.get(lock_name):
            with r.lock(lock_name, timeout=30):
                key = JIOCHAT_ACCESS_TOKEN_KEY % self.channel_uuid

                post_data = dict(grant_type="client_credentials", client_id=self.app_id, client_secret=self.app_secret)
                url = "https://channels.jiochat.com/auth/token.action"

                event = HttpEvent("POST", url, json.dumps(post_data))
                start = time.time()

                response = self._request(url, post_data, access_token=None)
                event.status_code = response.status_code

                if response.status_code != 200:
                    event.response_body = response.content
                    ChannelLog.log_channel_request(channel_id, "Got non-200 response from Jiochat", event, start, True)
                    return

                response_json = response.json()
                event.response_body = json.dumps(response_json)
                ChannelLog.log_channel_request(
                    channel_id, "Successfully fetched access token from Jiochat", event, start
                )

                access_token = response_json["access_token"]
                r.set(key, access_token, ex=7200)
                return access_token
Ejemplo n.º 9
0
    def start_call(self, call, to, from_, status_callback):
        if not settings.SEND_CALLS:
            raise ValueError("SEND_CALLS set to False, skipping call start")

        params = dict(to=to, from_=call.channel.address, url=status_callback, status_callback=status_callback)

        try:
            twilio_call = self.api.calls.create(**params)
            call.external_id = str(twilio_call.sid)

            # the call was successfully sent to the IVR provider
            call.status = IVRCall.WIRED
            call.save()

            for event in self.events:
                ChannelLog.log_ivr_interaction(call, "Started call", event)

        except TwilioRestException as twilio_error:
            message = "Twilio Error: %s" % twilio_error.msg
            if twilio_error.code == 20003:
                message = _("Could not authenticate with your Twilio account. Check your token and try again.")

            event = HttpEvent("POST", "https://api.nexmo.com/v1/calls", json.dumps(params), response_body=str(message))
            ChannelLog.log_ivr_interaction(call, "Call start failed", event, is_error=True)

            call.status = IVRCall.FAILED
            call.save()

            raise IVRException(message)
Ejemplo n.º 10
0
    def start_call(self, call, to, from_, status_callback):
        if not settings.SEND_CALLS:
            raise IVRException("SEND_CALLS set to False, skipping call start")

        try:

            #time.sleep(60)
            sleeptime = settings.SLEEP_TIME_CALL
            #if sleeptime>0:
            #time.sleep(sleeptime)
            print("Wait for %s seconde before every call" % sleeptime)
            twilio_call = self.calls.create(to=to,
                                            from_=call.channel.address,
                                            url=status_callback,
                                            status_callback=status_callback)
            call.external_id = six.text_type(twilio_call.sid)
            call.save()

            for event in self.calls.events:
                ChannelLog.log_ivr_interaction(call, 'Started call', event)
            if sleeptime > 0:
                time.sleep(sleeptime)
        except TwilioRestException as twilio_error:
            message = 'Twilio Error: %s' % twilio_error.msg
            if twilio_error.code == 20003:
                message = _(
                    'Could not authenticate with your Twilio account. Check your token and try again.'
                )

            raise IVRException(message)
Ejemplo n.º 11
0
    def start_call(self, call, to, from_, status_callback):
        if not settings.SEND_CALLS:
            raise ValueError("SEND_CALLS set to False, skipping call start")

        url = "https://%s%s" % (self.org.get_brand_domain(), reverse("ivr.ivrcall_handle", args=[call.pk]))

        params = dict()
        params["answer_url"] = [url]
        params["answer_method"] = "POST"
        params["to"] = [dict(type="phone", number=to.strip("+"))]
        params["from"] = dict(type="phone", number=from_.strip("+"))
        params["event_url"] = ["%s?has_event=1" % url]
        params["event_method"] = "POST"

        try:
            response = self.create_call(params=params)
            call_uuid = response.get("uuid", None)
            call.external_id = str(call_uuid)

            # the call was successfully sent to the IVR provider
            call.status = IVRCall.WIRED
            call.save()

            for event in self.events:
                ChannelLog.log_ivr_interaction(call, "Started call", event)

        except Exception as e:
            event = HttpEvent("POST", "https://api.nexmo.com/v1/calls", json.dumps(params), response_body=str(e))
            ChannelLog.log_ivr_interaction(call, "Call start failed", event, is_error=True)

            call.status = IVRCall.FAILED
            call.save()

            raise IVRException(_("Nexmo call failed, with error %s") % str(e))
Ejemplo n.º 12
0
    def download_media(self, call, media_url):
        """
        Fetches the recording and stores it with the provided recording_id
        :param media_url: the url where the media lives
        :return: the url for our downloaded media with full content type prefix
        """
        attempts = 0
        response = None
        while attempts < 4:
            response = self.download_recording(media_url)

            # in some cases Twilio isn't ready for us to fetch the recording URL yet, if we get a 404
            # sleep for a bit then try again up to 4 times
            if response.status_code == 200:
                break
            else:
                attempts += 1
                time.sleep(0.250)

        content_type, downloaded = self.org.save_response_media(response)

        if content_type:
            # log that we downloaded it to our own url
            request = response.request
            event = HttpEvent(request.method, request.url, request.body, response.status_code, downloaded)
            ChannelLog.log_ivr_interaction(call, "Downloaded media", event)

            return "%s:%s" % (content_type, downloaded)

        return None
Ejemplo n.º 13
0
    def get_user_detail(self, open_id, channel_id):
        access_token = self.get_access_token()

        url = 'https://channels.jiochat.com/user/info.action?'

        payload = dict(openid=open_id)
        event = HttpEvent('GET', url, json.dumps(payload))
        start = time.time()

        response = self._request(url, 'GET', payload, access_token)

        event.status_code = response.status_code

        if response.status_code != 200:
            event.response_body = response.content
            ChannelLog.log_channel_request(
                channel_id, "Got non-200 response from Jiochat", event, start,
                True)
            return dict()

        data = response.json()

        event.response_body = json.dumps(data)
        ChannelLog.log_channel_request(
            channel_id, "Successfully fetched user detail from Jiochat", event,
            start)

        return data
Ejemplo n.º 14
0
def check_failed_calls_task():
    from .models import IVRCall

    # calls that have failed and have a `error_count` value are going to be retried
    failed_calls_to_retry = (
        IVRCall.objects.filter(error_count__gte=1, error_count__lte=IVRCall.MAX_ERROR_COUNT)
        .filter(status__in=IVRCall.FAILED)
        .filter(modified_on__gt=timezone.now() - timedelta(days=IVRCall.IGNORE_PENDING_CALLS_OLDER_THAN_DAYS))
        .filter(direction=IVRCall.OUTGOING, is_active=True)
    )

    for call in failed_calls_to_retry:

        ChannelLog.log_ivr_interaction(call, "Retrying failed call", HttpEvent(method="INTERNAL", url=None))

        call.status = IVRCall.PENDING
        # reset the call
        call.started_on = None
        call.ended_on = None
        call.duration = 0
        call.modified_on = timezone.now()
        call.save(update_fields=("status", "next_attempt", "started_on", "ended_on", "duration", "modified_on"))

    if failed_calls_to_retry:
        task_enqueue_call_events.apply_async()
Ejemplo n.º 15
0
    def start_call(self, call, to, from_, status_callback):
        url = 'https://%s%s' % (settings.TEMBA_HOST,
                                reverse('ivr.ivrcall_handle', args=[call.pk]))

        params = dict()
        params['answer_url'] = [url]
        params['answer_method'] = 'POST'
        params['to'] = [dict(type='phone', number=to.strip('+'))]
        params['from'] = dict(type='phone', number=from_.strip('+'))
        params['event_url'] = ["%s?has_event=1" % url]
        params['event_method'] = "POST"

        try:
            response = self.create_call(params=params)
            call_uuid = response.get('uuid', None)
            call.external_id = six.text_type(call_uuid)
            call.save()
            for event in self.events:
                ChannelLog.log_ivr_interaction(call, 'Started call', event)

        except Exception as e:
            event = HttpEvent('POST',
                              'https://api.nexmo.com/v1/calls',
                              json.dumps(params),
                              response_body=six.text_type(e))
            ChannelLog.log_ivr_interaction(call,
                                           'Call start failed',
                                           event,
                                           is_error=True)

            call.status = IVRCall.FAILED
            call.save()
            raise IVRException(
                _("Nexmo call failed, with error %s") %
                six.text_type(e.message))
Ejemplo n.º 16
0
    def request_media(self, media_id, channel_id):
        access_token = self.get_access_token()

        url = 'https://channels.jiochat.com/media/download.action'

        payload = dict(media_id=media_id)

        event = HttpEvent('GET', url, json.dumps(payload))
        start = time.time()

        response = None

        attempts = 0
        while attempts < 4:
            response = self._request(url, 'GET', payload, access_token)

            # If we fail sleep for a bit then try again up to 4 times
            if response.status_code == 200:
                break
            else:
                attempts += 1
                time.sleep(.250)

        if response:
            event.status_code = response.status_code

        if not event.status_code or event.status_code != 200:
            ChannelLog.log_channel_request(channel_id, "Failed to get media from Jiochat", event, start, True)
        else:
            ChannelLog.log_channel_request(channel_id, "Successfully fetched media from Jiochat", event, start)

        return response
Ejemplo n.º 17
0
    def test_channellog(self):
        contact = self.create_contact("Test", "+250788383383")
        msg = Msg.create_outgoing(self.org, self.admin, contact, "This is a test message")
        msg = dict_to_struct('MockMsg', msg.as_task_json())

        with SegmentProfiler("Channel Log inserts (10,000)", self, force_profile=True):
            for i in range(10000):
                ChannelLog.log_success(msg, "Sent Message", method="GET", url="http://foo",
                                       request="GET http://foo", response="Ok", response_status="201")
Ejemplo n.º 18
0
    def start_call(self):
        from temba.ivr.tasks import start_call_task

        ChannelLog.log_ivr_interaction(self, "Call queued internally", HttpEvent(method="INTERNAL", url=None))

        self.status = IVRCall.QUEUED
        self.save()

        on_transaction_commit(lambda: start_call_task.delay(self.pk))
Ejemplo n.º 19
0
    def do_start_call(self, qs=None):
        client = self.channel.get_ivr_client()
        domain = self.channel.callback_domain

        from temba.ivr.clients import IVRException
        from temba.flows.models import ActionLog, FlowRun

        if client:
            try:
                url = "https://%s%s" % (domain, reverse("ivr.ivrcall_handle", args=[self.pk]))
                if qs:  # pragma: no cover
                    url = "%s?%s" % (url, qs)

                tel = None

                # if we are working with a test contact, look for user settings
                if self.contact.is_test:
                    user_settings = self.created_by.get_settings()
                    if user_settings.tel:
                        tel = user_settings.tel
                        run = FlowRun.objects.filter(connection=self)
                        if run:
                            ActionLog.create(run[0], "Placing test call to %s" % user_settings.get_tel_formatted())
                if not tel:
                    tel_urn = self.contact_urn
                    tel = tel_urn.path

                client.start_call(self, to=tel, from_=self.channel.address, status_callback=url)

            except IVRException as e:
                import traceback

                traceback.print_exc()

                if self.contact.is_test:
                    run = FlowRun.objects.filter(connection=self)
                    ActionLog.create(run[0], "Call ended. %s" % str(e))

            except Exception as e:  # pragma: no cover
                import traceback

                traceback.print_exc()

                ChannelLog.log_ivr_interaction(
                    self, "Call failed unexpectedly", HttpEvent(method="INTERNAL", url=None, response_body=str(e))
                )

                self.status = self.FAILED
                self.save()

                if self.contact.is_test:
                    run = FlowRun.objects.filter(connection=self)
                    ActionLog.create(run[0], "Call ended.")
Ejemplo n.º 20
0
    def download_media(self, call, media_url):
        """
        Fetches the recording and stores it with the provided recording_id
        :param media_url: the url where the media lives
        :return: the url for our downloaded media with full content type prefix
        """
        attempts = 0
        while attempts < 4:
            response = self.download_recording(media_url)

            # in some cases Twilio isn't ready for us to fetch the recording URL yet, if we get a 404
            # sleep for a bit then try again up to 4 times
            if response.status_code == 200:
                break
            else:
                attempts += 1
                time.sleep(.250)

        disposition = response.headers.get('Content-Disposition', None)
        content_type = response.headers.get('Content-Type', None)

        if content_type:
            extension = None
            if disposition == 'inline':
                extension = mimetypes.guess_extension(content_type)
                extension = extension.strip('.')
            elif disposition:
                filename = re.findall("filename=\"(.+)\"", disposition)[0]
                extension = filename.rpartition('.')[2]
            elif content_type == 'audio/x-wav':
                extension = 'wav'

            temp = NamedTemporaryFile(delete=True)
            temp.write(response.content)
            temp.flush()

            # save our file off
            downloaded = self.org.save_media(File(temp), extension)

            # log that we downloaded it to our own url
            request = response.request
            event = HttpEvent(request.method, request.url, request.body,
                              response.status_code, downloaded)
            ChannelLog.log_ivr_interaction(call, "Downloaded media", event)

            return '%s:%s' % (content_type, downloaded)

        return None
Ejemplo n.º 21
0
    def test_channellog(self):
        contact = self.create_contact("Test", "+250788383383")
        msg = Msg.create_outgoing(self.org, self.admin, contact,
                                  "This is a test message")
        msg = dict_to_struct('MockMsg', msg.as_task_json())

        with SegmentProfiler("Channel Log inserts (10,000)",
                             self,
                             force_profile=True):
            for i in range(10000):
                ChannelLog.log_success(msg,
                                       "Sent Message",
                                       method="GET",
                                       url="http://foo",
                                       request="GET http://foo",
                                       response="Ok",
                                       response_status="201")
Ejemplo n.º 22
0
    def refresh_access_token(self, channel_id):
        r = get_redis_connection()
        lock_name = self.TOKEN_REFRESH_LOCK % self.channel_uuid

        if not r.get(lock_name):
            with r.lock(lock_name, timeout=30):
                key = self.TOKEN_STORE_KEY % self.channel_uuid

                data = dict(grant_type="client_credential",
                            appid=self.app_id,
                            secret=self.app_secret)
                url = self.TOKEN_URL

                event = HttpEvent("GET", url + "?" + urlencode(data))
                start = time.time()

                response = requests.get(url, params=data, timeout=15)
                event.status_code = response.status_code

                if response.status_code != 200:
                    event.response_body = response.content
                    ChannelLog.log_channel_request(
                        channel_id,
                        "Got non-200 response from %s" % self.API_NAME, event,
                        start, True)
                    return

                response_json = response.json()
                has_error = False
                if response_json.get("errcode", -1) != 0:
                    has_error = True

                event.response_body = json.dumps(response_json)
                ChannelLog.log_channel_request(
                    channel_id, "Successfully fetched access token from %s" %
                    self.API_NAME, event, start, has_error)

                access_token = response_json.get("access_token", "")
                expires = response_json.get("expires_in", 7200)
                if access_token:
                    r.set(key, access_token, ex=int(expires))
                    return access_token
Ejemplo n.º 23
0
    def refresh_access_token(self, channel_id):
        r = get_redis_connection()
        lock_name = self.token_refresh_lock % self.channel_uuid

        if not r.get(lock_name):
            with r.lock(lock_name, timeout=30):
                key = self.token_store_key % self.channel_uuid

                post_data = {
                    "grant_type": "client_credentials",
                    "client_id": self.app_id,
                    "client_secret": self.app_secret,
                }
                url = self.token_url

                event = HttpEvent("POST", url, json.dumps(post_data))
                start = time.time()

                response = self._request(url, post_data, access_token=None)
                event.status_code = response.status_code

                if response.status_code != 200:
                    event.response_body = response.content
                    ChannelLog.log_channel_request(
                        channel_id,
                        f"Got non-200 response from {self.api_name}", event,
                        start, True)
                    return

                response_json = response.json()
                event.response_body = json.dumps(response_json)
                ChannelLog.log_channel_request(
                    channel_id,
                    f"Successfully fetched access token from {self.api_name}",
                    event, start)

                access_token = response_json["access_token"]
                expires = response_json.get("expires_in", 7200)
                r.set(key, access_token, ex=int(expires))
                return access_token
Ejemplo n.º 24
0
    def do_start_call(self, qs=None):
        client = self.channel.get_ivr_client()
        domain = self.channel.callback_domain

        from temba.ivr.clients import IVRException

        if client and domain:
            try:
                url = "https://%s%s" % (domain, reverse("ivr.ivrcall_handle", args=[self.pk]))
                if qs:  # pragma: no cover
                    url = "%s?%s" % (url, qs)

                tel_urn = self.contact_urn
                tel = tel_urn.path

                client.start_call(self, to=tel, from_=self.channel.address, status_callback=url)

            except IVRException as e:  # pragma: no cover
                logger.error(f"Could not start IVR call: {str(e)}", exc_info=True)

            except Exception as e:  # pragma: no cover
                logger.error(f"Could not start IVR call: {str(e)}", exc_info=True)

                ChannelLog.log_ivr_interaction(
                    self, "Call failed unexpectedly", HttpEvent(method="INTERNAL", url=None, response_body=str(e))
                )

                self.status = self.FAILED
                self.save(update_fields=("status",))

        # client or domain are not known
        else:
            ChannelLog.log_ivr_interaction(
                self,
                "Unknown client or domain",
                HttpEvent(method="INTERNAL", url=None, response_body=f"client={client} domain={domain}"),
            )

            self.status = self.FAILED
            self.save(update_fields=("status",))
Ejemplo n.º 25
0
    def refresh_access_token(self, channel_id):
        r = get_redis_connection()
        lock_name = self.token_refresh_lock % self.channel_uuid

        if not r.get(lock_name):
            with r.lock(lock_name, timeout=30):
                key = self.token_store_key % self.channel_uuid

                data = {"grant_type": "client_credential", "appid": self.app_id, "secret": self.app_secret}
                url = self.token_url

                event = HttpEvent("GET", url + "?" + urlencode(data))
                start = time.time()

                response = requests.get(url, params=data, timeout=15)
                event.status_code = response.status_code

                if response.status_code != 200:
                    event.response_body = response.content
                    ChannelLog.log_channel_request(
                        channel_id, f"Got non-200 response from {self.api_name}", event, start, True
                    )
                    return

                response_json = response.json()
                has_error = False
                if response_json.get("errcode", -1) != 0:
                    has_error = True

                event.response_body = json.dumps(response_json)
                ChannelLog.log_channel_request(
                    channel_id, f"Successfully fetched access token from {self.api_name}", event, start, has_error
                )

                access_token = response_json.get("access_token", "")
                expires = response_json.get("expires_in", 7200)
                if access_token:
                    r.set(key, access_token, ex=int(expires))
                    return access_token
Ejemplo n.º 26
0
    def refresh_access_token(self, channel_id):
        r = get_redis_connection()
        lock_name = self.TOKEN_REFRESH_LOCK % self.channel_uuid

        if not r.get(lock_name):
            with r.lock(lock_name, timeout=30):
                key = self.TOKEN_STORE_KEY % self.channel_uuid

                data = dict(grant_type="client_credential", appid=self.app_id, secret=self.app_secret)
                url = self.TOKEN_URL

                event = HttpEvent("GET", url + "?" + urlencode(data))
                start = time.time()

                response = requests.get(url, params=data, timeout=15)
                event.status_code = response.status_code

                if response.status_code != 200:
                    event.response_body = response.content
                    ChannelLog.log_channel_request(
                        channel_id, "Got non-200 response from %s" % self.API_NAME, event, start, True
                    )
                    return

                response_json = response.json()
                has_error = False
                if response_json.get("errcode", -1) != 0:
                    has_error = True

                event.response_body = json.dumps(response_json)
                ChannelLog.log_channel_request(
                    channel_id, "Successfully fetched access token from %s" % self.API_NAME, event, start, has_error
                )

                access_token = response_json.get("access_token", "")
                expires = response_json.get("expires_in", 7200)
                if access_token:
                    r.set(key, access_token, ex=int(expires))
                    return access_token
Ejemplo n.º 27
0
def task_enqueue_call_events():
    from .models import IVRCall

    r = get_redis_connection()

    pending_call_events = (IVRCall.objects.filter(
        status=IVRCall.PENDING).filter(direction=IVRCall.OUTGOING).filter(
            channel__is_active=True).filter(
                modified_on__gt=timezone.now() -
                timedelta(days=IVRCall.IGNORE_PENDING_CALLS_OLDER_THAN_DAYS)
            ).select_related("channel").order_by("modified_on")[:1000])

    for call in pending_call_events:

        # are we handling a call on a throttled channel ?
        max_concurrent_events = call.channel.config.get(
            Channel.CONFIG_MAX_CONCURRENT_EVENTS)

        if max_concurrent_events:
            channel_key = Channel.redis_active_events_key(call.channel_id)
            current_active_events = r.get(channel_key)

            # skip this call if are on the limit
            if current_active_events and int(
                    current_active_events) >= max_concurrent_events:
                continue
            else:
                # we can start a new call event
                call.register_active_event()

        # enqueue the call
        ChannelLog.log_ivr_interaction(call, "Call queued internally",
                                       HttpEvent(method="INTERNAL", url=None))

        call.status = IVRCall.QUEUED
        call.save(update_fields=("status", ))

        start_call_task.apply_async(kwargs={"call_pk": call.id})
Ejemplo n.º 28
0
    def start_call(self, call, to, from_, status_callback):
        if not settings.SEND_CALLS:
            raise IVRException("SEND_CALLS set to False, skipping call start")

        try:
            twilio_call = self.calls.create(to=to,
                                            from_=call.channel.address,
                                            url=status_callback,
                                            status_callback=status_callback)
            call.external_id = str(twilio_call.sid)
            call.save()

            for event in self.calls.events:
                ChannelLog.log_ivr_interaction(call, "Started call", event)

        except TwilioRestException as twilio_error:
            message = "Twilio Error: %s" % twilio_error.msg
            if twilio_error.code == 20003:
                message = _(
                    "Could not authenticate with your Twilio account. Check your token and try again."
                )

            raise IVRException(message)
Ejemplo n.º 29
0
def task_enqueue_call_events():
    from .models import IVRCall

    r = get_redis_connection()

    pending_call_events = (
        IVRCall.objects.filter(status=IVRCall.PENDING)
        .filter(direction=IVRCall.OUTGOING, is_active=True)
        .filter(channel__is_active=True)
        .filter(modified_on__gt=timezone.now() - timedelta(days=IVRCall.IGNORE_PENDING_CALLS_OLDER_THAN_DAYS))
        .select_related("channel")
        .order_by("modified_on")[:1000]
    )

    for call in pending_call_events:

        # are we handling a call on a throttled channel ?
        max_concurrent_events = call.channel.config.get(Channel.CONFIG_MAX_CONCURRENT_EVENTS)

        if max_concurrent_events:
            channel_key = Channel.redis_active_events_key(call.channel_id)
            current_active_events = r.get(channel_key)

            # skip this call if are on the limit
            if current_active_events and int(current_active_events) >= max_concurrent_events:
                continue
            else:
                # we can start a new call event
                call.register_active_event()

        # enqueue the call
        ChannelLog.log_ivr_interaction(call, "Call queued internally", HttpEvent(method="INTERNAL", url=None))

        call.status = IVRCall.QUEUED
        call.save(update_fields=("status",))

        start_call_task.apply_async(kwargs={"call_pk": call.id}, queue=Queue.HANDLER)
Ejemplo n.º 30
0
    def handle_group_inbound(self, request, uuid, data):
        from warapidpro.types import WhatsAppGroupType
        channel = self.lookup_channel(WhatsAppGroupType.code, uuid)
        if not channel:
            error_msg = "Channel not found for id: %s" % (uuid, )
            logger.error(error_msg)
            return HttpResponse(error_msg, status=400)

        from_addr = data['from_addr']
        content = self.get_content(data)
        attachments = self.get_attachments(data)
        group_uuid = data.get('group', {}).get('uuid')

        # The group webhook receives messages for all groups,
        # only grab the message if it's a group we're a channel for.
        if channel.config_json()['group_uuid'] != group_uuid:
            logger.info('Received message for a different group.')
            return JsonResponse({}, status=200)

        message = Msg.create_incoming(channel,
                                      URN.from_tel(from_addr),
                                      content,
                                      external_id=data['uuid'],
                                      attachments=attachments)

        response_body = {
            'message_id': message.pk,
        }

        request_body = request.body
        request_method = request.method
        request_path = request.get_full_path()

        event = HttpEvent(request_method, request_path, request_body, 201,
                          json.dumps(response_body))
        ChannelLog.log_message(message, 'Handled inbound message.', event)
        return JsonResponse(response_body, status=201)
Ejemplo n.º 31
0
 def hangup(self, call):
     self.update_call(call.external_id, action="hangup", call_id=call.external_id)
     for event in self.events:
         ChannelLog.log_ivr_interaction(call, "Hung up call", event)
Ejemplo n.º 32
0
 def hangup(self, call):
     response = self.calls.hangup(call.external_id)
     for event in self.calls.events:
         ChannelLog.log_ivr_interaction(call, 'Hung up call', event)
     return response
Ejemplo n.º 33
0
    def post(self, request, *args, **kwargs):
        from temba.msgs.models import Msg

        request_body = request.body
        request_method = request.method
        request_path = request.get_full_path()

        def log_channel(channel, description, event, is_error=False):
            return ChannelLog.objects.create(
                channel_id=channel.pk,
                is_error=is_error,
                request=event.request_body,
                response=event.response_body,
                url=event.url,
                method=event.method,
                response_status=event.status_code,
                description=description,
            )

        action = kwargs["action"].lower()
        request_uuid = kwargs["uuid"]

        data = json.loads(force_text(request_body))
        is_ussd = self.is_ussd_message(data)
        channel_data = data.get("channel_data", {})
        channel_types = ("JNU", "JN")

        # look up the channel
        channel = Channel.objects.filter(uuid=request_uuid, is_active=True, channel_type__in=channel_types).first()

        if not channel:
            return HttpResponse("Channel not found for id: %s" % request_uuid, status=400)

        auth = request.META.get("HTTP_AUTHORIZATION", "").split(" ")
        secret = channel.config.get(Channel.CONFIG_SECRET)
        if secret is not None and (len(auth) != 2 or auth[0] != "Token" or auth[1] != secret):
            return JsonResponse(dict(error="Incorrect authentication token"), status=401)

        # Junebug is sending an event
        if action == "event":
            expected_keys = ["event_type", "message_id", "timestamp"]
            if not set(expected_keys).issubset(data.keys()):
                status = 400
                response_body = "Missing one of %s in request parameters." % (", ".join(expected_keys))
                event = HttpEvent(request_method, request_path, request_body, status, response_body)
                log_channel(channel, "Failed to handle event.", event, is_error=True)
                return HttpResponse(response_body, status=status)

            message_id = data["message_id"]
            event_type = data["event_type"]

            # look up the message
            message = Msg.objects.filter(channel=channel, external_id=message_id).select_related("channel")
            if not message:
                status = 400
                response_body = "Message with external id of '%s' not found" % (message_id,)
                event = HttpEvent(request_method, request_path, request_body, status, response_body)
                log_channel(channel, "Failed to handle %s event_type." % (event_type), event)
                return HttpResponse(response_body, status=status)

            if event_type == "submitted":
                for message_obj in message:
                    message_obj.status_sent()
            if event_type == "delivery_succeeded":
                for message_obj in message:
                    message_obj.status_delivered()
            elif event_type in ["delivery_failed", "rejected"]:
                for message_obj in message:
                    message_obj.status_fail()

            response_body = {"status": self.ACK, "message_ids": [message_obj.pk for message_obj in message]}
            event = HttpEvent(request_method, request_path, request_body, 200, json.dumps(response_body))
            log_channel(channel, "Handled %s event_type." % (event_type), event)
            # Let Junebug know we're happy
            return JsonResponse(response_body)

        # Handle an inbound message
        elif action == "inbound":
            expected_keys = [
                "channel_data",
                "from",
                "channel_id",
                "timestamp",
                "content",
                "to",
                "reply_to",
                "message_id",
            ]
            if not set(expected_keys).issubset(data.keys()):
                status = 400
                response_body = "Missing one of %s in request parameters." % (", ".join(expected_keys))
                event = HttpEvent(request_method, request_path, request_body, status, response_body)
                log_channel(channel, "Failed to handle message.", event, is_error=True)
                return HttpResponse(response_body, status=status)

            if is_ussd:
                status = {"close": USSDSession.INTERRUPTED, "new": USSDSession.TRIGGERED}.get(
                    channel_data.get("session_event"), USSDSession.IN_PROGRESS
                )

                message_date = datetime.strptime(data["timestamp"], "%Y-%m-%d %H:%M:%S.%f")
                gmt_date = pytz.timezone("GMT").localize(message_date)
                # Use a session id if provided, otherwise fall back to using the `from` address as the identifier
                session_id = channel_data.get("session_id") or data["from"]

                connection = USSDSession.handle_incoming(
                    channel=channel,
                    urn=data["from"],
                    content=data["content"],
                    status=status,
                    date=gmt_date,
                    external_id=session_id,
                    message_id=data["message_id"],
                    starcode=data["to"],
                )

                if connection:
                    status = 200
                    response_body = {"status": self.ACK, "session_id": connection.pk}
                    event = HttpEvent(request_method, request_path, request_body, status, json.dumps(response_body))
                    log_channel(
                        channel, "Handled USSD message of %s session_event" % (channel_data["session_event"],), event
                    )
                    return JsonResponse(response_body, status=status)
                else:
                    status = 400
                    response_body = {"status": self.NACK, "reason": "No suitable session found for this message."}
                    event = HttpEvent(request_method, request_path, request_body, status, json.dumps(response_body))
                    log_channel(
                        channel,
                        "Failed to handle USSD message of %s session_event" % (channel_data["session_event"],),
                        event,
                    )
                    return JsonResponse(response_body, status=status)
            else:
                content = data["content"]
                message = Msg.create_incoming(channel, URN.from_tel(data["from"]), content)
                status = 200
                response_body = {"status": self.ACK, "message_id": message.pk}
                Msg.objects.filter(pk=message.id).update(external_id=data["message_id"])
                event = HttpEvent(request_method, request_path, request_body, status, json.dumps(response_body))
                ChannelLog.log_message(message, "Handled inbound message.", event)
                return JsonResponse(response_body, status=status)
Ejemplo n.º 34
0
    def get(self, request, *args, **kwargs):
        from temba.flows.models import FlowSession
        from temba.ivr.models import IVRCall

        action = kwargs["action"].lower()

        request_body = force_text(request.body)
        request_path = request.get_full_path()
        request_method = request.method

        request_uuid = kwargs["uuid"]

        if action == "event":
            if not request_body:
                return HttpResponse("")

            body_json = json.loads(request_body)
            status = body_json.get("status", None)
            duration = body_json.get("duration", None)
            call_uuid = body_json.get("uuid", None)
            conversation_uuid = body_json.get("conversation_uuid", None)

            if call_uuid is None:
                return HttpResponse("Missing uuid parameter, ignoring")

            call = IVRCall.objects.filter(external_id=call_uuid).first()
            if not call:
                # try looking up by the conversation uuid (inbound calls start with that)
                call = IVRCall.objects.filter(external_id=conversation_uuid).first()
                if call:
                    call.external_id = call_uuid
                    call.save()
                else:
                    response = dict(message="Call not found for %s" % call_uuid)
                    return JsonResponse(response)

            channel = call.channel
            channel_type = channel.channel_type
            call.update_status(status, duration, channel_type)
            call.save()

            response = dict(
                description="Updated call status", call=dict(status=call.get_status_display(), duration=call.duration)
            )

            event = HttpEvent(request_method, request_path, request_body, 200, json.dumps(response))
            ChannelLog.log_ivr_interaction(call, "Updated call status", event)

            if call.status == IVRCall.COMPLETED:
                # if our call is completed, hangup
                runs = FlowRun.objects.filter(connection=call)
                for run in runs:
                    if not run.is_completed():
                        run.set_completed(exit_uuid=None)

            return JsonResponse(response)

        if action == "answer":
            if not request_body:
                return HttpResponse("")

            body_json = json.loads(request_body)
            from_number = body_json.get("from", None)
            channel_number = body_json.get("to", None)
            external_id = body_json.get("conversation_uuid", None)

            if not from_number or not channel_number or not external_id:
                return HttpResponse("Missing parameters, Ignoring")

            # look up the channel
            address_q = Q(address=channel_number) | Q(address=("+" + channel_number))
            channel = Channel.objects.filter(address_q).filter(is_active=True, channel_type="NX").first()

            # make sure we got one, and that it matches the key for our org
            org_uuid = None
            if channel:
                org_uuid = channel.org.config.get(NEXMO_UUID, None)

            if not channel or org_uuid != request_uuid:
                return HttpResponse("Channel not found for number: %s" % channel_number, status=404)

            urn = URN.from_tel(from_number)
            contact, urn_obj = Contact.get_or_create(channel.org, urn, channel)

            flow = Trigger.find_flow_for_inbound_call(contact)

            if flow:
                call = IVRCall.create_incoming(channel, contact, urn_obj, channel.created_by, external_id)
                session = FlowSession.create(contact, connection=call)

                FlowRun.create(flow, contact, session=session, connection=call)
                response = Flow.handle_call(call)
                channel_type = channel.channel_type
                call.update_status("answered", None, channel_type)

                event = HttpEvent(request_method, request_path, request_body, 200, str(response))
                ChannelLog.log_ivr_interaction(call, "Incoming request for call", event)
                return JsonResponse(json.loads(str(response)), safe=False)
            else:
                # we don't have an inbound trigger to deal with this call.
                response = channel.generate_ivr_response()

                # say nothing and hangup, this is a little rude, but if we reject the call, then
                # they'll get a non-working number error. We send 'busy' when our server is down
                # so we don't want to use that here either.
                response.say("")
                response.hangup()

                # if they have a missed call trigger, fire that off
                Trigger.catch_triggers(contact, Trigger.TYPE_MISSED_CALL, channel)

                # either way, we need to hangup now
                return JsonResponse(json.loads(str(response)), safe=False)
Ejemplo n.º 35
0
    def do_start_call(self, qs=None):
        client = self.channel.get_ivr_client()
        domain = self.channel.callback_domain

        from temba.ivr.clients import IVRException
        from temba.flows.models import ActionLog, FlowRun

        if client and domain:
            try:
                url = "https://%s%s" % (domain, reverse("ivr.ivrcall_handle", args=[self.pk]))
                if qs:  # pragma: no cover
                    url = "%s?%s" % (url, qs)

                tel = None

                # if we are working with a test contact, look for user settings
                if self.contact.is_test:
                    user_settings = self.created_by.get_settings()
                    if user_settings.tel:
                        tel = user_settings.tel
                        run = FlowRun.objects.filter(connection=self)
                        if run:
                            ActionLog.create(run[0], "Placing test call to %s" % user_settings.get_tel_formatted())
                if not tel:
                    tel_urn = self.contact_urn
                    tel = tel_urn.path

                client.start_call(self, to=tel, from_=self.channel.address, status_callback=url)

            except IVRException as e:
                import traceback

                traceback.print_exc()

                if self.contact.is_test:
                    run = FlowRun.objects.filter(connection=self)
                    ActionLog.create(run[0], "Call ended. %s" % str(e))

            except Exception as e:  # pragma: no cover
                import traceback

                traceback.print_exc()

                ChannelLog.log_ivr_interaction(
                    self, "Call failed unexpectedly", HttpEvent(method="INTERNAL", url=None, response_body=str(e))
                )

                self.status = self.FAILED
                self.save(update_fields=("status",))

                if self.contact.is_test:
                    run = FlowRun.objects.filter(connection=self)
                    ActionLog.create(run[0], "Call ended.")

        # client or domain are not known
        else:
            ChannelLog.log_ivr_interaction(
                self,
                "Unknown client or domain",
                HttpEvent(method="INTERNAL", url=None, response_body=f"client={client} domain={domain}"),
            )

            self.status = self.FAILED
            self.save(update_fields=("status",))
Ejemplo n.º 36
0
    def post(self, request, *args, **kwargs):
        from temba.msgs.models import Msg

        request_body = request.body
        request_method = request.method
        request_path = request.get_full_path()

        def log_channel(channel, description, event, is_error=False):
            return ChannelLog.objects.create(
                channel_id=channel.pk,
                is_error=is_error,
                request=event.request_body,
                response=event.response_body,
                url=event.url,
                method=event.method,
                response_status=event.status_code,
                description=description,
            )

        action = kwargs["action"].lower()
        request_uuid = kwargs["uuid"]

        data = json.loads(force_text(request_body))
        is_ussd = self.is_ussd_message(data)
        channel_data = data.get("channel_data", {})
        channel_types = ("JNU", "JN")

        # look up the channel
        channel = Channel.objects.filter(
            uuid=request_uuid, is_active=True,
            channel_type__in=channel_types).first()

        if not channel:
            return HttpResponse("Channel not found for id: %s" % request_uuid,
                                status=400)

        auth = request.META.get("HTTP_AUTHORIZATION", "").split(" ")
        secret = channel.config.get(Channel.CONFIG_SECRET)
        if secret is not None and (len(auth) != 2 or auth[0] != "Token"
                                   or auth[1] != secret):
            return JsonResponse(dict(error="Incorrect authentication token"),
                                status=401)

        # Junebug is sending an event
        if action == "event":
            expected_keys = ["event_type", "message_id", "timestamp"]
            if not set(expected_keys).issubset(data.keys()):
                status = 400
                response_body = "Missing one of %s in request parameters." % (
                    ", ".join(expected_keys))
                event = HttpEvent(request_method, request_path, request_body,
                                  status, response_body)
                log_channel(channel,
                            "Failed to handle event.",
                            event,
                            is_error=True)
                return HttpResponse(response_body, status=status)

            message_id = data["message_id"]
            event_type = data["event_type"]

            # look up the message
            message = Msg.objects.filter(
                channel=channel,
                external_id=message_id).select_related("channel")
            if not message:
                status = 400
                response_body = "Message with external id of '%s' not found" % (
                    message_id, )
                event = HttpEvent(request_method, request_path, request_body,
                                  status, response_body)
                log_channel(channel,
                            "Failed to handle %s event_type." % (event_type),
                            event)
                return HttpResponse(response_body, status=status)

            if event_type == "submitted":
                for message_obj in message:
                    message_obj.status_sent()
            if event_type == "delivery_succeeded":
                for message_obj in message:
                    message_obj.status_delivered()
            elif event_type in ["delivery_failed", "rejected"]:
                for message_obj in message:
                    message_obj.status_fail()

            response_body = {
                "status": self.ACK,
                "message_ids": [message_obj.pk for message_obj in message]
            }
            event = HttpEvent(request_method, request_path, request_body, 200,
                              json.dumps(response_body))
            log_channel(channel, "Handled %s event_type." % (event_type),
                        event)
            # Let Junebug know we're happy
            return JsonResponse(response_body)

        # Handle an inbound message
        elif action == "inbound":
            expected_keys = [
                "channel_data",
                "from",
                "channel_id",
                "timestamp",
                "content",
                "to",
                "reply_to",
                "message_id",
            ]
            if not set(expected_keys).issubset(data.keys()):
                status = 400
                response_body = "Missing one of %s in request parameters." % (
                    ", ".join(expected_keys))
                event = HttpEvent(request_method, request_path, request_body,
                                  status, response_body)
                log_channel(channel,
                            "Failed to handle message.",
                            event,
                            is_error=True)
                return HttpResponse(response_body, status=status)

            if is_ussd:
                status = {
                    "close": USSDSession.INTERRUPTED,
                    "new": USSDSession.TRIGGERED
                }.get(channel_data.get("session_event"),
                      USSDSession.IN_PROGRESS)

                message_date = datetime.strptime(data["timestamp"],
                                                 "%Y-%m-%d %H:%M:%S.%f")
                gmt_date = pytz.timezone("GMT").localize(message_date)
                # Use a session id if provided, otherwise fall back to using the `from` address as the identifier
                session_id = channel_data.get("session_id") or data["from"]

                connection = USSDSession.handle_incoming(
                    channel=channel,
                    urn=data["from"],
                    content=data["content"],
                    status=status,
                    date=gmt_date,
                    external_id=session_id,
                    message_id=data["message_id"],
                    starcode=data["to"],
                )

                if connection:
                    status = 200
                    response_body = {
                        "status": self.ACK,
                        "session_id": connection.pk
                    }
                    event = HttpEvent(request_method, request_path,
                                      request_body, status,
                                      json.dumps(response_body))
                    log_channel(
                        channel, "Handled USSD message of %s session_event" %
                        (channel_data["session_event"], ), event)
                    return JsonResponse(response_body, status=status)
                else:
                    status = 400
                    response_body = {
                        "status": self.NACK,
                        "reason": "No suitable session found for this message."
                    }
                    event = HttpEvent(request_method, request_path,
                                      request_body, status,
                                      json.dumps(response_body))
                    log_channel(
                        channel,
                        "Failed to handle USSD message of %s session_event" %
                        (channel_data["session_event"], ),
                        event,
                    )
                    return JsonResponse(response_body, status=status)
            else:
                content = data["content"]
                message = Msg.create_incoming(channel,
                                              URN.from_tel(data["from"]),
                                              content)
                status = 200
                response_body = {"status": self.ACK, "message_id": message.pk}
                Msg.objects.filter(pk=message.id).update(
                    external_id=data["message_id"])
                event = HttpEvent(request_method, request_path, request_body,
                                  status, json.dumps(response_body))
                ChannelLog.log_message(message, "Handled inbound message.",
                                       event)
                return JsonResponse(response_body, status=status)
Ejemplo n.º 37
0
 def hangup(self, call):
     twilio_call = self.api.calls.get(call.external_id).update(status="completed")
     for event in self.events:
         ChannelLog.log_ivr_interaction(call, "Hung up call", event)
     return twilio_call
Ejemplo n.º 38
0
    def get(self, request, *args, **kwargs):
        from temba.flows.models import FlowSession
        from temba.ivr.models import IVRCall

        action = kwargs["action"].lower()

        request_body = force_text(request.body)
        request_path = request.get_full_path()
        request_method = request.method

        request_uuid = kwargs["uuid"]

        if action == "event":
            if not request_body:
                return HttpResponse("")

            body_json = json.loads(request_body)
            status = body_json.get("status", None)
            duration = body_json.get("duration", None)
            call_uuid = body_json.get("uuid", None)
            conversation_uuid = body_json.get("conversation_uuid", None)

            if call_uuid is None:
                return HttpResponse("Missing uuid parameter, ignoring")

            call = IVRCall.objects.filter(external_id=call_uuid).first()
            if not call:
                # try looking up by the conversation uuid (inbound calls start with that)
                call = IVRCall.objects.filter(
                    external_id=conversation_uuid).first()
                if call:
                    call.external_id = call_uuid
                    call.save()
                else:
                    response = dict(message="Call not found for %s" %
                                    call_uuid)
                    return JsonResponse(response)

            channel = call.channel
            channel_type = channel.channel_type
            call.update_status(status, duration, channel_type)
            call.save()

            response = dict(description="Updated call status",
                            call=dict(status=call.get_status_display(),
                                      duration=call.duration))

            event = HttpEvent(request_method, request_path, request_body, 200,
                              json.dumps(response))
            ChannelLog.log_ivr_interaction(call, "Updated call status", event)

            if call.status == IVRCall.COMPLETED:
                # if our call is completed, hangup
                runs = FlowRun.objects.filter(connection=call)
                for run in runs:
                    if not run.is_completed():
                        run.set_completed(exit_uuid=None)

            return JsonResponse(response)

        if action == "answer":
            if not request_body:
                return HttpResponse("")

            body_json = json.loads(request_body)
            from_number = body_json.get("from", None)
            channel_number = body_json.get("to", None)
            external_id = body_json.get("conversation_uuid", None)

            if not from_number or not channel_number or not external_id:
                return HttpResponse("Missing parameters, Ignoring")

            # look up the channel
            address_q = Q(address=channel_number) | Q(address=("+" +
                                                               channel_number))
            channel = Channel.objects.filter(address_q).filter(
                is_active=True, channel_type="NX").first()

            # make sure we got one, and that it matches the key for our org
            org_uuid = None
            if channel:
                org_uuid = channel.org.config.get(NEXMO_UUID, None)

            if not channel or org_uuid != request_uuid:
                return HttpResponse("Channel not found for number: %s" %
                                    channel_number,
                                    status=404)

            urn = URN.from_tel(from_number)
            contact, urn_obj = Contact.get_or_create(channel.org, urn, channel)

            flow = Trigger.find_flow_for_inbound_call(contact)

            if flow:
                call = IVRCall.create_incoming(channel, contact, urn_obj,
                                               channel.created_by, external_id)
                session = FlowSession.create(contact, connection=call)

                FlowRun.create(flow, contact, session=session, connection=call)
                response = Flow.handle_call(call)
                channel_type = channel.channel_type
                call.update_status("answered", None, channel_type)

                event = HttpEvent(request_method, request_path, request_body,
                                  200, str(response))
                ChannelLog.log_ivr_interaction(call,
                                               "Incoming request for call",
                                               event)
                return JsonResponse(json.loads(str(response)), safe=False)
            else:
                # we don't have an inbound trigger to deal with this call.
                response = channel.generate_ivr_response()

                # say nothing and hangup, this is a little rude, but if we reject the call, then
                # they'll get a non-working number error. We send 'busy' when our server is down
                # so we don't want to use that here either.
                response.say("")
                response.hangup()

                # if they have a missed call trigger, fire that off
                Trigger.catch_triggers(contact, Trigger.TYPE_MISSED_CALL,
                                       channel)

                # either way, we need to hangup now
                return JsonResponse(json.loads(str(response)), safe=False)
Ejemplo n.º 39
0
    def post(self, request, *args, **kwargs):
        call = IVRCall.objects.filter(pk=kwargs["pk"]).first()

        if not call:
            return HttpResponse("Not found", status=404)

        channel = call.channel

        if not (channel.is_active and channel.org):
            return HttpResponse("No channel found", status=400)

        channel_type = channel.channel_type
        ivr_protocol = Channel.get_type_from_code(channel_type).ivr_protocol
        client = channel.get_ivr_client()

        request_body = force_text(request.body)
        request_method = request.method
        request_path = request.get_full_path()

        if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_TWIML and request.POST.get(
                "hangup", 0):
            if not request.user.is_anonymous:
                user_org = request.user.get_org()
                if user_org and user_org.pk == call.org.pk:
                    client.hangup(call)
                    return HttpResponse(json.dumps(dict(status="Canceled")),
                                        content_type="application/json")
                else:  # pragma: no cover
                    return HttpResponse("Not found", status=404)

        input_redirect = "1" == request.GET.get("input_redirect", "0")
        if client.validate(request):
            status = None
            duration = None
            if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_TWIML:
                status = request.POST.get("CallStatus", None)
                duration = request.POST.get("CallDuration", None)
            elif ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_NCCO:
                if request_body:
                    body_json = json.loads(request_body)
                    status = body_json.get("status", None)
                    duration = body_json.get("duration", None)

                # force in progress call status for fake (input) redirects
                if input_redirect:
                    status = "answered"

            # nexmo does not set status for some callbacks
            if status is not None:
                call.update_status(
                    status, duration, channel_type
                )  # update any calls we have spawned with the same
                call.save()

            resume = request.GET.get("resume", 0)
            user_response = request.POST.copy()

            hangup = False
            saved_media_url = None
            text = None
            media_url = None
            has_event = False

            if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_TWIML:

                # figure out if this is a callback due to an empty gather
                is_empty = "1" == request.GET.get("empty", "0")

                # if the user pressed pound, then record no digits as the input
                if is_empty:
                    user_response["Digits"] = ""

                hangup = "hangup" == user_response.get("Digits", None)

                media_url = user_response.get("RecordingUrl", None)
                # if we've been sent a recording, go grab it
                if media_url:
                    saved_media_url = client.download_media(media_url)

                # parse the user response
                text = user_response.get("Digits", None)

            elif ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_NCCO:
                if request_body:
                    body_json = json.loads(request_body)
                    media_url = body_json.get("recording_url", None)

                    if media_url:
                        cache.set("last_call:media_url:%d" % call.pk,
                                  media_url, None)

                    media_url = cache.get("last_call:media_url:%d" % call.pk,
                                          None)
                    text = body_json.get("dtmf", None)
                    if input_redirect:
                        text = None

                has_event = "1" == request.GET.get("has_event", "0")
                save_media = "1" == request.GET.get("save_media", "0")
                if media_url:
                    if save_media:
                        saved_media_url = client.download_media(
                            call, media_url)
                        cache.delete("last_call:media_url:%d" % call.pk)
                    else:
                        response_msg = "Saved media url"
                        response = dict(message=response_msg)

                        event = HttpEvent(request_method, request_path,
                                          request_body, 200,
                                          json.dumps(response))
                        ChannelLog.log_ivr_interaction(call, response_msg,
                                                       event)
                        return JsonResponse(response)

            if not has_event and call.status not in IVRCall.DONE or hangup:
                if call.is_ivr():
                    response = Flow.handle_call(
                        call,
                        text=text,
                        saved_media_url=saved_media_url,
                        hangup=hangup,
                        resume=resume)
                    event = HttpEvent(request_method, request_path,
                                      request_body, 200, str(response))
                    if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_NCCO:
                        ChannelLog.log_ivr_interaction(
                            call, "Incoming request for call", event)

                        # TODO: what's special here that this needs to be different?
                        return JsonResponse(json.loads(str(response)),
                                            safe=False)

                    ChannelLog.log_ivr_interaction(
                        call, "Incoming request for call", event)
                    return HttpResponse(str(response),
                                        content_type="text/xml; charset=utf-8")
            else:

                if call.status == IVRCall.COMPLETED:
                    # if our call is completed, hangup
                    runs = FlowRun.objects.filter(connection=call)
                    for run in runs:
                        if not run.is_completed():
                            run.set_completed(exit_uuid=None)

                response = dict(
                    description="Updated call status",
                    call=dict(status=call.get_status_display(),
                              duration=call.duration),
                )

                event = HttpEvent(request_method, request_path, request_body,
                                  200, json.dumps(response))
                ChannelLog.log_ivr_interaction(call, "Updated call status",
                                               event)
                return JsonResponse(response)

        else:  # pragma: no cover

            error = "Invalid request signature"
            event = HttpEvent(request_method, request_path, request_body, 200,
                              error)
            ChannelLog.log_ivr_interaction(call, error, event, is_error=True)
            # raise an exception that things weren't properly signed
            raise ValidationError(error)

        return JsonResponse(dict(message="Unhandled"))  # pragma: no cover
Ejemplo n.º 40
0
    def post(self, request, *args, **kwargs):
        call = IVRCall.objects.filter(pk=kwargs["pk"]).first()

        if not call:
            return HttpResponse("Not found", status=404)

        channel = call.channel

        if not (channel.is_active and channel.org):
            return HttpResponse("No channel found", status=400)

        channel_type = channel.channel_type
        ivr_protocol = Channel.get_type_from_code(channel_type).ivr_protocol
        client = channel.get_ivr_client()

        request_body = force_text(request.body)
        request_method = request.method
        request_path = request.get_full_path()

        if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_TWIML and request.POST.get("hangup", 0):
            if not request.user.is_anonymous:
                user_org = request.user.get_org()
                if user_org and user_org.pk == call.org.pk:
                    client.hangup(call)
                    return HttpResponse(json.dumps(dict(status="Canceled")), content_type="application/json")
                else:  # pragma: no cover
                    return HttpResponse("Not found", status=404)

        input_redirect = "1" == request.GET.get("input_redirect", "0")
        if client.validate(request):
            status = None
            duration = None
            if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_TWIML:
                status = request.POST.get("CallStatus", None)
                duration = request.POST.get("CallDuration", None)
            elif ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_NCCO:
                if request_body:
                    body_json = json.loads(request_body)
                    status = body_json.get("status", None)
                    duration = body_json.get("duration", None)

                # force in progress call status for fake (input) redirects
                if input_redirect:
                    status = "answered"

            # nexmo does not set status for some callbacks
            if status is not None:
                call.update_status(status, duration, channel_type)  # update any calls we have spawned with the same
                call.save()

            resume = request.GET.get("resume", 0)
            user_response = request.POST.copy()

            hangup = False
            saved_media_url = None
            text = None
            media_url = None
            has_event = False

            if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_TWIML:

                # figure out if this is a callback due to an empty gather
                is_empty = "1" == request.GET.get("empty", "0")

                # if the user pressed pound, then record no digits as the input
                if is_empty:
                    user_response["Digits"] = ""

                hangup = "hangup" == user_response.get("Digits", None)

                media_url = user_response.get("RecordingUrl", None)
                # if we've been sent a recording, go grab it
                if media_url:
                    saved_media_url = client.download_media(media_url)

                # parse the user response
                text = user_response.get("Digits", None)

            elif ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_NCCO:
                if request_body:
                    body_json = json.loads(request_body)
                    media_url = body_json.get("recording_url", None)

                    if media_url:
                        cache.set("last_call:media_url:%d" % call.pk, media_url, None)

                    media_url = cache.get("last_call:media_url:%d" % call.pk, None)
                    text = body_json.get("dtmf", None)
                    if input_redirect:
                        text = None

                has_event = "1" == request.GET.get("has_event", "0")
                save_media = "1" == request.GET.get("save_media", "0")
                if media_url:
                    if save_media:
                        saved_media_url = client.download_media(call, media_url)
                        cache.delete("last_call:media_url:%d" % call.pk)
                    else:
                        response_msg = "Saved media url"
                        response = dict(message=response_msg)

                        event = HttpEvent(request_method, request_path, request_body, 200, json.dumps(response))
                        ChannelLog.log_ivr_interaction(call, response_msg, event)
                        return JsonResponse(response)

            if not has_event and call.status not in IVRCall.DONE or hangup:
                if call.is_ivr():
                    response = Flow.handle_call(
                        call, text=text, saved_media_url=saved_media_url, hangup=hangup, resume=resume
                    )
                    event = HttpEvent(request_method, request_path, request_body, 200, str(response))
                    if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_NCCO:
                        ChannelLog.log_ivr_interaction(call, "Incoming request for call", event)

                        # TODO: what's special here that this needs to be different?
                        return JsonResponse(json.loads(str(response)), safe=False)

                    ChannelLog.log_ivr_interaction(call, "Incoming request for call", event)
                    return HttpResponse(str(response), content_type="text/xml; charset=utf-8")
            else:

                if call.status == IVRCall.COMPLETED:
                    # if our call is completed, hangup
                    runs = FlowRun.objects.filter(connection=call)
                    for run in runs:
                        if not run.is_completed():
                            run.set_completed(exit_uuid=None)

                response = dict(
                    description="Updated call status",
                    call=dict(status=call.get_status_display(), duration=call.duration),
                )

                event = HttpEvent(request_method, request_path, request_body, 200, json.dumps(response))
                ChannelLog.log_ivr_interaction(call, "Updated call status", event)
                return JsonResponse(response)

        else:  # pragma: no cover

            error = "Invalid request signature"
            event = HttpEvent(request_method, request_path, request_body, 200, error)
            ChannelLog.log_ivr_interaction(call, error, event, is_error=True)
            # raise an exception that things weren't properly signed
            raise ValidationError(error)

        return JsonResponse(dict(message="Unhandled"))  # pragma: no cover
Ejemplo n.º 41
0
    def post(self, request, *args, **kwargs):
        from temba.msgs.models import Msg

        request_body = request.body
        request_method = request.method
        request_path = request.get_full_path()

        def log_channel(channel, description, event, is_error=False):
            return ChannelLog.objects.create(channel_id=channel.pk,
                                             is_error=is_error,
                                             request=event.request_body,
                                             response=event.response_body,
                                             url=event.url,
                                             method=event.method,
                                             response_status=event.status_code,
                                             description=description)

        action = kwargs['action'].lower()
        request_uuid = kwargs['uuid']

        data = json.loads(force_text(request_body))
        is_ussd = self.is_ussd_message(data)
        channel_data = data.get('channel_data', {})
        channel_types = ('JNU', 'JN')

        # look up the channel
        channel = Channel.objects.filter(
            uuid=request_uuid, is_active=True,
            channel_type__in=channel_types).first()

        if not channel:
            return HttpResponse("Channel not found for id: %s" % request_uuid,
                                status=400)

        auth = request.META.get('HTTP_AUTHORIZATION', '').split(' ')
        secret = channel.config.get(Channel.CONFIG_SECRET)
        if secret is not None and (len(auth) != 2 or auth[0] != 'Token'
                                   or auth[1] != secret):
            return JsonResponse(dict(error="Incorrect authentication token"),
                                status=401)

        # Junebug is sending an event
        if action == 'event':
            expected_keys = ["event_type", "message_id", "timestamp"]
            if not set(expected_keys).issubset(data.keys()):
                status = 400
                response_body = "Missing one of %s in request parameters." % (
                    ', '.join(expected_keys))
                event = HttpEvent(request_method, request_path, request_body,
                                  status, response_body)
                log_channel(channel,
                            'Failed to handle event.',
                            event,
                            is_error=True)
                return HttpResponse(response_body, status=status)

            message_id = data['message_id']
            event_type = data["event_type"]

            # look up the message
            message = Msg.objects.filter(
                channel=channel,
                external_id=message_id).select_related('channel')
            if not message:
                status = 400
                response_body = "Message with external id of '%s' not found" % (
                    message_id, )
                event = HttpEvent(request_method, request_path, request_body,
                                  status, response_body)
                log_channel(channel,
                            'Failed to handle %s event_type.' % (event_type),
                            event)
                return HttpResponse(response_body, status=status)

            if event_type == 'submitted':
                for message_obj in message:
                    message_obj.status_sent()
            if event_type == 'delivery_succeeded':
                for message_obj in message:
                    message_obj.status_delivered()
            elif event_type in ['delivery_failed', 'rejected']:
                for message_obj in message:
                    message_obj.status_fail()

            response_body = {
                'status': self.ACK,
                'message_ids': [message_obj.pk for message_obj in message]
            }
            event = HttpEvent(request_method, request_path, request_body, 200,
                              json.dumps(response_body))
            log_channel(channel, 'Handled %s event_type.' % (event_type),
                        event)
            # Let Junebug know we're happy
            return JsonResponse(response_body)

        # Handle an inbound message
        elif action == 'inbound':
            expected_keys = [
                'channel_data',
                'from',
                'channel_id',
                'timestamp',
                'content',
                'to',
                'reply_to',
                'message_id',
            ]
            if not set(expected_keys).issubset(data.keys()):
                status = 400
                response_body = "Missing one of %s in request parameters." % (
                    ', '.join(expected_keys))
                event = HttpEvent(request_method, request_path, request_body,
                                  status, response_body)
                log_channel(channel,
                            'Failed to handle message.',
                            event,
                            is_error=True)
                return HttpResponse(response_body, status=status)

            if is_ussd:
                status = {
                    'close': USSDSession.INTERRUPTED,
                    'new': USSDSession.TRIGGERED,
                }.get(channel_data.get('session_event'),
                      USSDSession.IN_PROGRESS)

                message_date = datetime.strptime(data['timestamp'],
                                                 "%Y-%m-%d %H:%M:%S.%f")
                gmt_date = pytz.timezone('GMT').localize(message_date)
                # Use a session id if provided, otherwise fall back to using the `from` address as the identifier
                session_id = channel_data.get('session_id') or data['from']

                connection = USSDSession.handle_incoming(
                    channel=channel,
                    urn=data['from'],
                    content=data['content'],
                    status=status,
                    date=gmt_date,
                    external_id=session_id,
                    message_id=data['message_id'],
                    starcode=data['to'])

                if connection:
                    status = 200
                    response_body = {
                        'status': self.ACK,
                        'session_id': connection.pk,
                    }
                    event = HttpEvent(request_method, request_path,
                                      request_body, status,
                                      json.dumps(response_body))
                    log_channel(
                        channel, 'Handled USSD message of %s session_event' %
                        (channel_data['session_event'], ), event)
                    return JsonResponse(response_body, status=status)
                else:
                    status = 400
                    response_body = {
                        'status': self.NACK,
                        'reason': 'No suitable session found for this message.'
                    }
                    event = HttpEvent(request_method, request_path,
                                      request_body, status,
                                      json.dumps(response_body))
                    log_channel(
                        channel,
                        'Failed to handle USSD message of %s session_event' %
                        (channel_data['session_event'], ), event)
                    return JsonResponse(response_body, status=status)
            else:
                content = data['content']
                message = Msg.create_incoming(channel,
                                              URN.from_tel(data['from']),
                                              content)
                status = 200
                response_body = {
                    'status': self.ACK,
                    'message_id': message.pk,
                }
                Msg.objects.filter(pk=message.id).update(
                    external_id=data['message_id'])
                event = HttpEvent(request_method, request_path, request_body,
                                  status, json.dumps(response_body))
                ChannelLog.log_message(message, 'Handled inbound message.',
                                       event)
                return JsonResponse(response_body, status=status)
Ejemplo n.º 42
0
    def post(self, request, *args, **kwargs):
        call = IVRCall.objects.filter(pk=kwargs['pk']).first()

        if not call:
            return HttpResponse("Not found", status=404)

        channel = call.channel
        channel_type = channel.channel_type
        client = channel.get_ivr_client()

        request_body = request.body
        request_method = request.method
        request_path = request.get_full_path()

        if channel_type in Channel.TWIML_CHANNELS and request.POST.get(
                'hangup', 0):
            if not request.user.is_anonymous():
                user_org = request.user.get_org()
                if user_org and user_org.pk == call.org.pk:
                    client.calls.hangup(call.external_id)
                    return HttpResponse(json.dumps(dict(status='Canceled')),
                                        content_type="application/json")
                else:  # pragma: no cover
                    return HttpResponse("Not found", status=404)

        input_redirect = '1' == request.GET.get('input_redirect', '0')
        if client.validate(request):
            status = None
            duration = None
            if channel_type in Channel.TWIML_CHANNELS:
                status = request.POST.get('CallStatus', None)
                duration = request.POST.get('CallDuration', None)
            elif channel_type in Channel.NCCO_CHANNELS:
                if request_body:
                    body_json = json.loads(request_body)
                    status = body_json.get('status', None)
                    duration = body_json.get('duration', None)

                # force in progress call status for fake (input) redirects
                if input_redirect:
                    status = 'answered'

            call.update_status(
                status, duration,
                channel_type)  # update any calls we have spawned with the same
            call.save()

            resume = request.GET.get('resume', 0)
            user_response = request.POST.copy()

            hangup = False
            saved_media_url = None
            text = None
            media_url = None

            if channel_type in Channel.TWIML_CHANNELS:

                # figure out if this is a callback due to an empty gather
                is_empty = '1' == request.GET.get('empty', '0')

                # if the user pressed pound, then record no digits as the input
                if is_empty:
                    user_response['Digits'] = ''

                hangup = 'hangup' == user_response.get('Digits', None)

                media_url = user_response.get('RecordingUrl', None)
                # if we've been sent a recording, go grab it
                if media_url:
                    saved_media_url = client.download_media(media_url)

                # parse the user response
                text = user_response.get('Digits', None)

            elif channel_type in Channel.NCCO_CHANNELS:
                if request_body:
                    body_json = json.loads(request_body)
                    media_url = body_json.get('recording_url', None)

                    if media_url:
                        cache.set('last_call:media_url:%d' % call.pk,
                                  media_url, None)

                    media_url = cache.get('last_call:media_url:%d' % call.pk,
                                          None)
                    text = body_json.get('dtmf', None)
                    if input_redirect:
                        text = None

                has_event = '1' == request.GET.get('has_event', '0')
                if has_event:
                    return HttpResponse(six.text_type(''))

                save_media = '1' == request.GET.get('save_media', '0')
                if media_url:
                    if save_media:
                        saved_media_url = client.download_media(media_url)
                        cache.delete('last_call:media_url:%d' % call.pk)
                    else:
                        response_msg = 'media URL saved'
                        ChannelLog.log_ivr_interaction(
                            call, "Saved media URL", request_body,
                            six.text_type(response_msg), request_path,
                            request_method)
                        return HttpResponse(six.text_type(response_msg))

            if call.status not in IVRCall.DONE or hangup:
                if call.is_ivr():
                    response = Flow.handle_call(
                        call,
                        text=text,
                        saved_media_url=saved_media_url,
                        hangup=hangup,
                        resume=resume)
                    if channel_type in Channel.NCCO_CHANNELS:

                        ChannelLog.log_ivr_interaction(call,
                                                       "Returned response",
                                                       request_body,
                                                       six.text_type(response),
                                                       request_path,
                                                       request_method)
                        return JsonResponse(json.loads(
                            six.text_type(response)),
                                            safe=False)

                    ChannelLog.log_ivr_interaction(call, "Returned response",
                                                   request_body,
                                                   six.text_type(response),
                                                   request_path,
                                                   request_method)
                    return HttpResponse(six.text_type(response))
            else:
                if call.status == IVRCall.COMPLETED:
                    # if our call is completed, hangup
                    run = FlowRun.objects.filter(session=call).first()
                    if run:
                        run.set_completed()

                response = dict(message="Updated call status",
                                call=dict(status=call.get_status_display(),
                                          duration=call.duration))
                ChannelLog.log_ivr_interaction(
                    call,
                    "Updated call status: %s" % call.get_status_display(),
                    request_body, json.dumps(response), request_path,
                    request_method)
                return JsonResponse(response)

        else:  # pragma: no cover

            error_message = "Invalid request signature"
            ChannelLog.log_ivr_interaction(call,
                                           error_message,
                                           request_body,
                                           error_message,
                                           request_path,
                                           request_method,
                                           is_error=True)
            # raise an exception that things weren't properly signed
            raise ValidationError(error_message)

        return JsonResponse(dict(message="Unhandled"))  # pragma: no cover