Example #1
0
    def update_status(self, status: str, duration: float, channel_type: str):
        """
        Updates our status from a provide call status string

        """
        if not status:
            raise ValueError(f"IVR Call status must be defined, got: '{status}'")

        previous_status = self.status

        from temba.flows.models import FlowRun

        ivr_protocol = Channel.get_type_from_code(channel_type).ivr_protocol
        if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_TWIML:
            self.status = self.derive_ivr_status_twiml(status, previous_status)
        elif ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_NCCO:
            self.status = self.derive_ivr_status_nexmo(status, previous_status)
        else:  # pragma: no cover
            raise ValueError(f"Unhandled IVR protocol: {ivr_protocol}")

        # if we are in progress, mark our start time
        if self.status == self.IN_PROGRESS and previous_status != self.IN_PROGRESS:
            self.started_on = timezone.now()

        # if we are done, mark our ended time
        if self.status in ChannelConnection.DONE:
            self.ended_on = timezone.now()

            self.unregister_active_event()

            from temba.flows.models import FlowSession

            if self.has_flow_session():
                self.session.end(FlowSession.STATUS_COMPLETED)

        if self.status in ChannelConnection.RETRY_CALL and previous_status not in ChannelConnection.RETRY_CALL:
            flow = self.get_flow()
            backoff_minutes = flow.metadata.get("ivr_retry", IVRCall.RETRY_BACKOFF_MINUTES)

            self.schedule_call_retry(backoff_minutes)

        if duration is not None:
            self.duration = duration

        # if we are moving into IN_PROGRESS, make sure our runs have proper expirations
        if previous_status in (self.PENDING, self.QUEUED, self.WIRED) and self.status in (
            self.IN_PROGRESS,
            self.RINGING,
        ):
            runs = FlowRun.objects.filter(connection=self, is_active=True)
            for run in runs:
                if not run.expires_on or (
                    run.expires_on - run.modified_on > timedelta(minutes=self.IVR_EXPIRES_CHOICES[-1][0])
                ):
                    run.update_expiration()

        if self.status == ChannelConnection.FAILED:
            flow = self.get_flow()
            if flow.metadata.get("ivr_retry_failed_events"):
                self.schedule_failed_call_retry()
Example #2
0
    def update_status(self, status, duration, channel_type):
        """
        Updates our status from a provide call status string

        """
        from temba.flows.models import FlowRun, ActionLog

        previous_status = self.status
        ivr_protocol = Channel.get_type_from_code(channel_type).ivr_protocol

        if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_TWIML:
            if status == 'queued':
                self.status = self.QUEUED
            elif status == 'ringing':
                self.status = self.RINGING
            elif status == 'no-answer':
                self.status = self.NO_ANSWER
            elif status == 'in-progress':
                if self.status != self.IN_PROGRESS:
                    self.started_on = timezone.now()
                self.status = self.IN_PROGRESS
            elif status == 'completed':
                if self.contact.is_test:
                    run = FlowRun.objects.filter(connection=self)
                    if run:
                        ActionLog.create(run[0], _("Call ended."))
                self.status = self.COMPLETED
            elif status == 'busy':
                self.status = self.BUSY
            elif status == 'failed':
                self.status = self.FAILED
            elif status == 'canceled':
                self.status = self.CANCELED

        elif ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_NCCO:
            if status in ('ringing', 'started'):
                self.status = self.RINGING
            elif status == 'answered':
                self.status = self.IN_PROGRESS
            elif status == 'completed':
                self.status = self.COMPLETED
            elif status == 'failed':
                self.status = self.FAILED
            elif status in ('rejected', 'busy'):
                self.status = self.BUSY
            elif status in ('unanswered', 'timeout'):
                self.status = self.NO_ANSWER

        # if we are done, mark our ended time
        if self.status in ChannelSession.DONE:
            self.ended_on = timezone.now()

        if duration is not None:
            self.duration = duration

        # if we are moving into IN_PROGRESS, make sure our runs have proper expirations
        if previous_status in [self.QUEUED, self.PENDING] and self.status in [self.IN_PROGRESS, self.RINGING]:
            runs = FlowRun.objects.filter(connection=self, is_active=True, expires_on=None)
            for run in runs:
                run.update_expiration()
Example #3
0
    def create_channel(
        self,
        channel_type: str,
        name: str,
        address: str,
        role=None,
        schemes=None,
        country=None,
        secret=None,
        config=None,
        org=None,
    ):
        channel_type = Channel.get_type_from_code(channel_type)

        return Channel.objects.create(
            org=org or self.org,
            country=country,
            channel_type=channel_type.code,
            name=name,
            address=address,
            config=config or {},
            role=role or Channel.DEFAULT_ROLE,
            secret=secret,
            schemes=schemes or channel_type.schemes,
            created_by=self.admin,
            modified_by=self.admin,
        )
Example #4
0
    def update_status(self, status: str, duration: float, channel_type: str):
        """
        Updates our status from a provide call status string

        """
        if not status:
            raise ValueError(
                f"IVR Call status must be defined, got: '{status}'")

        previous_status = self.status

        from temba.flows.models import FlowRun, ActionLog

        ivr_protocol = Channel.get_type_from_code(channel_type).ivr_protocol
        if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_TWIML:
            self.status = self.derive_ivr_status_twiml(status, previous_status)
        elif ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_NCCO:
            self.status = self.derive_ivr_status_nexmo(status, previous_status)
        else:  # pragma: no cover
            raise ValueError(f"Unhandled IVR protocol: {ivr_protocol}")

        # if we are in progress, mark our start time
        if self.status == self.IN_PROGRESS and previous_status != self.IN_PROGRESS:
            self.started_on = timezone.now()

        # if we are done, mark our ended time
        if self.status in ChannelSession.DONE:
            self.ended_on = timezone.now()

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

        if self.status in ChannelSession.RETRY_CALL and previous_status not in ChannelSession.RETRY_CALL:
            flow = self.get_flow()
            backoff_minutes = flow.metadata.get("ivr_retry",
                                                IVRCall.RETRY_BACKOFF_MINUTES)

            self.schedule_call_retry(backoff_minutes)

        if duration is not None:
            self.duration = duration

        # if we are moving into IN_PROGRESS, make sure our runs have proper expirations
        if previous_status in (self.PENDING, self.QUEUED,
                               self.WIRED) and self.status in (
                                   self.IN_PROGRESS,
                                   self.RINGING,
                               ):
            runs = FlowRun.objects.filter(connection=self,
                                          is_active=True,
                                          expires_on=None)
            for run in runs:
                run.update_expiration()
Example #5
0
    def handle_simulate(self, num_runs, org_id, flow_name, seed):
        """
        Prepares to resume simulating flow activity on an existing database
        """
        self._log(
            "Resuming flow activity simulation on existing database...\n")

        orgs = Org.objects.order_by('id')
        if org_id:
            orgs = orgs.filter(id=org_id)

        if not orgs:
            raise CommandError("Can't simulate activity on an empty database")

        self.configure_random(len(orgs), seed)

        # in real life Nexmo messages are throttled, but that's not necessary for this simulation
        Channel.get_type_from_code('NX').max_tps = None

        inputs_by_flow_name = {f['name']: f['templates'] for f in FLOWS}

        self._log("Preparing existing orgs... ")

        for org in orgs:
            flows = org.flows.order_by('id')

            if flow_name:
                flows = flows.filter(name=flow_name)
            flows = list(flows)

            for flow in flows:
                flow.input_templates = inputs_by_flow_name[flow.name]

            org.cache = {
                'users':
                list(org.get_org_users().order_by('id')),
                'channels':
                list(org.channels.order_by('id')),
                'groups':
                list(ContactGroup.user_groups.filter(org=org).order_by('id')),
                'flows':
                flows,
                'contacts':
                list(org.org_contacts.values_list(
                    'id', flat=True)),  # only ids to save memory
                'activity':
                None
            }

        self._log(self.style.SUCCESS("OK") + '\n')

        self.simulate_activity(orgs, num_runs)
Example #6
0
    def update_status(self, status: str, duration: float, channel_type: str):
        """
        Updates our status from a provide call status string

        """
        if not status:
            raise ValueError(f"IVR Call status must be defined, got: '{status}'")

        previous_status = self.status

        from temba.flows.models import FlowRun, ActionLog

        ivr_protocol = Channel.get_type_from_code(channel_type).ivr_protocol
        if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_TWIML:
            self.status = self.derive_ivr_status_twiml(status, previous_status)
        elif ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_NCCO:
            self.status = self.derive_ivr_status_nexmo(status, previous_status)
        else:  # pragma: no cover
            raise ValueError(f"Unhandled IVR protocol: {ivr_protocol}")

        # if we are in progress, mark our start time
        if self.status == self.IN_PROGRESS and previous_status != self.IN_PROGRESS:
            self.started_on = timezone.now()

        # if we are done, mark our ended time
        if self.status in ChannelSession.DONE:
            self.ended_on = timezone.now()

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

        if self.status in ChannelSession.RETRY_CALL and previous_status not in ChannelSession.RETRY_CALL:
            flow = self.get_flow()
            backoff_minutes = flow.metadata.get("ivr_retry", IVRCall.RETRY_BACKOFF_MINUTES)

            self.schedule_call_retry(backoff_minutes)

        if duration is not None:
            self.duration = duration

        # if we are moving into IN_PROGRESS, make sure our runs have proper expirations
        if previous_status in (self.PENDING, self.QUEUED, self.WIRED) and self.status in (
            self.IN_PROGRESS,
            self.RINGING,
        ):
            runs = FlowRun.objects.filter(connection=self, is_active=True, expires_on=None)
            for run in runs:
                run.update_expiration()
Example #7
0
    def handle_simulate(self, num_runs, org_id, flow_name, seed):
        """
        Prepares to resume simulating flow activity on an existing database
        """
        self._log("Resuming flow activity simulation on existing database...\n")

        orgs = Org.objects.order_by("id")
        if org_id:
            orgs = orgs.filter(id=org_id)

        if not orgs:
            raise CommandError("Can't simulate activity on an empty database")

        self.configure_random(len(orgs), seed)

        # in real life Nexmo messages are throttled, but that's not necessary for this simulation
        Channel.get_type_from_code("NX").max_tps = None

        inputs_by_flow_name = {f["name"]: f["templates"] for f in FLOWS}

        self._log("Preparing existing orgs... ")

        for org in orgs:
            flows = org.flows.order_by("id").exclude(is_system=True)

            if flow_name:
                flows = flows.filter(name=flow_name)
            flows = list(flows)

            for flow in flows:
                flow.input_templates = inputs_by_flow_name[flow.name]

            org.cache = {
                "users": list(org.get_org_users().order_by("id")),
                "channels": list(org.channels.order_by("id")),
                "groups": list(ContactGroup.user_groups.filter(org=org).order_by("id")),
                "flows": flows,
                "contacts": list(org.org_contacts.values_list("id", flat=True)),  # only ids to save memory
                "activity": None,
            }

        self._log(self.style.SUCCESS("OK") + "\n")

        self.simulate_activity(orgs, num_runs)
Example #8
0
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        is_support = self.request.user.groups.filter(
            name="Customer Support").first()

        end = timezone.now()
        begin = end - timedelta(days=30)
        begin = self.request.GET.get("begin",
                                     datetime.strftime(begin, "%Y-%m-%d"))
        end = self.request.GET.get("end", datetime.strftime(end, "%Y-%m-%d"))

        direction = self.request.GET.get("direction", "IO")

        if begin and end:
            orgs = []
            org = self.derive_org()
            if org:
                orgs = Org.objects.filter(Q(id=org.id) | Q(parent=org))

            count_types = []
            if "O" in direction:
                count_types = [
                    ChannelCount.OUTGOING_MSG_TYPE,
                    ChannelCount.OUTGOING_IVR_TYPE
                ]

            if "I" in direction:
                count_types += [
                    ChannelCount.INCOMING_MSG_TYPE,
                    ChannelCount.INCOMING_IVR_TYPE
                ]

            # get all our counts for that period
            daily_counts = (ChannelCount.objects.filter(
                count_type__in=count_types).filter(day__gte=begin).filter(
                    day__lte=end).exclude(channel__org=None))
            if orgs:
                daily_counts = daily_counts.filter(channel__org__in=orgs)

            context["orgs"] = list(
                daily_counts.values(
                    "channel__org",
                    "channel__org__name").order_by("-count_sum").annotate(
                        count_sum=Sum("count"))[:12])

            channel_types = (ChannelCount.objects.filter(
                count_type__in=count_types).filter(day__gte=begin).filter(
                    day__lte=end).exclude(channel__org=None))

            if orgs or not is_support:
                channel_types = channel_types.filter(channel__org__in=orgs)

            channel_types = list(
                channel_types.values("channel__channel_type").order_by(
                    "-count_sum").annotate(count_sum=Sum("count")))

            # populate the channel names
            pie = []
            for channel_type in channel_types[0:6]:
                channel_type["channel__name"] = Channel.get_type_from_code(
                    channel_type["channel__channel_type"]).name
                pie.append(channel_type)

            other_count = 0
            for channel_type in channel_types[6:]:
                other_count += channel_type["count_sum"]

            if other_count:
                pie.append(dict(channel__name="Other", count_sum=other_count))

            context["channel_types"] = pie

            context["begin"] = datetime.strptime(begin, "%Y-%m-%d").date()
            context["end"] = datetime.strptime(end, "%Y-%m-%d").date()
            context["direction"] = direction

        return context
Example #9
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
Example #10
0
    def send(self, channel, msg, text):
        connection = None

        # if the channel config has specified and override hostname use that, otherwise use settings
        event_hostname = channel.config.get(Channel.CONFIG_RP_HOSTNAME_OVERRIDE, settings.HOSTNAME)

        # the event url Junebug will relay events to
        event_url = 'http://%s%s' % (event_hostname, reverse('courier.jn', args=[channel.uuid, 'event']))

        is_ussd = Channel.get_type_from_code(channel.channel_type).category == ChannelType.Category.USSD

        # build our payload
        payload = {'event_url': event_url, 'content': text}

        if channel.secret is not None:
            payload['event_auth_token'] = channel.secret

        if is_ussd:
            connection = USSDSession.objects.get_with_status_only(msg.connection_id)
            # make sure USSD responses are only valid for a short window
            response_expiration = timezone.now() - timedelta(seconds=180)
            external_id = None
            if msg.response_to_id and msg.created_on > response_expiration:
                external_id = Msg.objects.values_list('external_id', flat=True).filter(pk=msg.response_to_id).first()
            # NOTE: Only one of `to` or `reply_to` may be specified, use external_id if we have it.
            if external_id:
                payload['reply_to'] = external_id
            else:
                payload['to'] = msg.urn_path
            payload['channel_data'] = {
                'continue_session': connection and not connection.should_end or False,
            }
        else:
            payload['from'] = channel.address
            payload['to'] = msg.urn_path

        log_url = channel.config[Channel.CONFIG_SEND_URL]
        start = time.time()

        event = HttpEvent('POST', log_url, json.dumps(payload))
        headers = {'Content-Type': 'application/json'}
        headers.update(TEMBA_HEADERS)

        try:
            response = requests.post(
                channel.config[Channel.CONFIG_SEND_URL], verify=True,
                json=payload, timeout=15, headers=headers,
                auth=(channel.config[Channel.CONFIG_USERNAME],
                      channel.config[Channel.CONFIG_PASSWORD]))

            event.status_code = response.status_code
            event.response_body = response.text

        except Exception as e:
            raise SendException(unicode(e), event=event, start=start)

        if not (200 <= response.status_code < 300):
            raise SendException("Received a non 200 response %d from Junebug" % response.status_code,
                                event=event, start=start)

        data = response.json()

        if is_ussd and connection and connection.should_end:
            connection.close()

        try:
            message_id = data['result']['message_id']
            Channel.success(channel, msg, WIRED, start, event=event, external_id=message_id)
        except KeyError, e:
            raise SendException("Unable to read external message_id: %r" % (e,),
                                event=HttpEvent('POST', log_url,
                                                request_body=json.dumps(json.dumps(payload)),
                                                response_body=json.dumps(data)),
                                start=start)
Example #11
0
 def send(self, channel, msg, text):
     # use regular Vumi channel sending
     return Channel.get_type_from_code('VM').send(channel, msg, text)
Example #12
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
Example #13
0
 def send(self, channel, msg, text):
     # use regular Junebug channel sending
     return Channel.get_type_from_code('JN').send(channel, msg, text)
Example #14
0
    def send(self, channel, msg, text):
        connection = None

        # if the channel config has specified and override hostname use that, otherwise use settings
        callback_domain = channel.config.get(
            Channel.CONFIG_RP_HOSTNAME_OVERRIDE, None)
        if not callback_domain:
            callback_domain = channel.callback_domain

        # the event url Junebug will relay events to
        event_url = "http://%s%s" % (
            callback_domain,
            reverse("handlers.junebug_handler", args=["event", channel.uuid]),
        )

        is_ussd = Channel.get_type_from_code(
            channel.channel_type).category == ChannelType.Category.USSD

        # build our payload
        payload = {"event_url": event_url, "content": text}

        secret = channel.config.get(Channel.CONFIG_SECRET)
        if secret is not None:
            payload["event_auth_token"] = secret

        connection = USSDSession.objects.get_with_status_only(
            msg.connection_id)

        # make sure USSD responses are only valid for a short window
        response_expiration = timezone.now() - timedelta(seconds=180)
        external_id = None
        if msg.response_to_id and msg.created_on > response_expiration:
            external_id = Msg.objects.values_list(
                "external_id",
                flat=True).filter(pk=msg.response_to_id).first()
        # NOTE: Only one of `to` or `reply_to` may be specified, use external_id if we have it.
        if external_id:
            payload["reply_to"] = external_id
        else:
            payload["to"] = msg.urn_path
        payload["channel_data"] = {
            "continue_session": connection and not connection.should_end
            or False
        }

        log_url = channel.config[Channel.CONFIG_SEND_URL]
        start = time.time()

        event = HttpEvent("POST", log_url, json.dumps(payload))
        headers = http_headers(extra={"Content-Type": "application/json"})

        try:
            response = requests.post(
                channel.config[Channel.CONFIG_SEND_URL],
                verify=True,
                json=payload,
                timeout=15,
                headers=headers,
                auth=(channel.config[Channel.CONFIG_USERNAME],
                      channel.config[Channel.CONFIG_PASSWORD]),
            )

            event.status_code = response.status_code
            event.response_body = response.text

        except Exception as e:
            raise SendException(str(e), event=event, start=start)

        if not (200 <= response.status_code < 300):
            raise SendException("Received a non 200 response %d from Junebug" %
                                response.status_code,
                                event=event,
                                start=start)

        data = response.json()

        if is_ussd and connection and connection.should_end:
            connection.close()

        try:
            message_id = data["result"]["message_id"]
            Channel.success(channel,
                            msg,
                            WIRED,
                            start,
                            event=event,
                            external_id=message_id)
        except KeyError as e:
            raise SendException(
                "Unable to read external message_id: %r" % (e, ),
                event=HttpEvent("POST",
                                log_url,
                                request_body=json.dumps(json.dumps(payload)),
                                response_body=json.dumps(data)),
                start=start,
            )
Example #15
0
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        is_support = self.request.user.groups.filter(name="Customer Support").first()

        end = timezone.now()
        begin = end - timedelta(days=30)
        begin = self.request.GET.get("begin", datetime.strftime(begin, "%Y-%m-%d"))
        end = self.request.GET.get("end", datetime.strftime(end, "%Y-%m-%d"))

        direction = self.request.GET.get("direction", "IO")

        if begin and end:
            orgs = []
            org = self.derive_org()
            if org:
                orgs = Org.objects.filter(Q(id=org.id) | Q(parent=org))

            count_types = []
            if "O" in direction:
                count_types = [ChannelCount.OUTGOING_MSG_TYPE, ChannelCount.OUTGOING_IVR_TYPE]

            if "I" in direction:
                count_types += [ChannelCount.INCOMING_MSG_TYPE, ChannelCount.INCOMING_IVR_TYPE]

            # get all our counts for that period
            daily_counts = (
                ChannelCount.objects.filter(count_type__in=count_types)
                .filter(day__gte=begin)
                .filter(day__lte=end)
                .exclude(channel__org=None)
            )
            if orgs:
                daily_counts = daily_counts.filter(channel__org__in=orgs)

            context["orgs"] = list(
                daily_counts.values("channel__org", "channel__org__name")
                .order_by("-count_sum")
                .annotate(count_sum=Sum("count"))[:12]
            )

            channel_types = (
                ChannelCount.objects.filter(count_type__in=count_types)
                .filter(day__gte=begin)
                .filter(day__lte=end)
                .exclude(channel__org=None)
            )

            if orgs or not is_support:
                channel_types = channel_types.filter(channel__org__in=orgs)

            channel_types = list(
                channel_types.values("channel__channel_type").order_by("-count_sum").annotate(count_sum=Sum("count"))
            )

            # populate the channel names
            pie = []
            for channel_type in channel_types[0:6]:
                channel_type["channel__name"] = Channel.get_type_from_code(channel_type["channel__channel_type"]).name
                pie.append(channel_type)

            other_count = 0
            for channel_type in channel_types[6:]:
                other_count += channel_type["count_sum"]

            if other_count:
                pie.append(dict(channel__name="Other", count_sum=other_count))

            context["channel_types"] = pie

            context["begin"] = datetime.strptime(begin, "%Y-%m-%d").date()
            context["end"] = datetime.strptime(end, "%Y-%m-%d").date()
            context["direction"] = direction

        return context
Example #16
0
    def send(self, channel, msg, text):
        connection = None

        # if the channel config has specified and override hostname use that, otherwise use settings
        callback_domain = channel.config.get(Channel.CONFIG_RP_HOSTNAME_OVERRIDE, None)
        if not callback_domain:
            callback_domain = channel.callback_domain

        # the event url Junebug will relay events to
        event_url = "http://%s%s" % (
            callback_domain,
            reverse("handlers.junebug_handler", args=["event", channel.uuid]),
        )

        is_ussd = Channel.get_type_from_code(channel.channel_type).category == ChannelType.Category.USSD

        # build our payload
        payload = {"event_url": event_url, "content": text}

        secret = channel.config.get(Channel.CONFIG_SECRET)
        if secret is not None:
            payload["event_auth_token"] = secret

        connection = USSDSession.objects.get_with_status_only(msg.connection_id)

        # make sure USSD responses are only valid for a short window
        response_expiration = timezone.now() - timedelta(seconds=180)
        external_id = None
        if msg.response_to_id and msg.created_on > response_expiration:
            external_id = Msg.objects.values_list("external_id", flat=True).filter(pk=msg.response_to_id).first()
        # NOTE: Only one of `to` or `reply_to` may be specified, use external_id if we have it.
        if external_id:
            payload["reply_to"] = external_id
        else:
            payload["to"] = msg.urn_path
        payload["channel_data"] = {"continue_session": connection and not connection.should_end or False}

        log_url = channel.config[Channel.CONFIG_SEND_URL]
        start = time.time()

        event = HttpEvent("POST", log_url, json.dumps(payload))
        headers = http_headers(extra={"Content-Type": "application/json"})

        try:
            response = requests.post(
                channel.config[Channel.CONFIG_SEND_URL],
                verify=True,
                json=payload,
                timeout=15,
                headers=headers,
                auth=(channel.config[Channel.CONFIG_USERNAME], channel.config[Channel.CONFIG_PASSWORD]),
            )

            event.status_code = response.status_code
            event.response_body = response.text

        except Exception as e:
            raise SendException(str(e), event=event, start=start)

        if not (200 <= response.status_code < 300):
            raise SendException(
                "Received a non 200 response %d from Junebug" % response.status_code, event=event, start=start
            )

        data = response.json()

        if is_ussd and connection and connection.should_end:
            connection.close()

        try:
            message_id = data["result"]["message_id"]
            Channel.success(channel, msg, WIRED, start, event=event, external_id=message_id)
        except KeyError as e:
            raise SendException(
                "Unable to read external message_id: %r" % (e,),
                event=HttpEvent(
                    "POST", log_url, request_body=json.dumps(json.dumps(payload)), response_body=json.dumps(data)
                ),
                start=start,
            )
Example #17
0
    def send(self, channel, msg, text):

        is_ussd = Channel.get_type_from_code(
            channel.channel_type).category == ChannelType.Category.USSD
        channel.config[
            'transport_name'] = 'ussd_transport' if is_ussd else 'mtech_ng_smpp_transport'

        session = None
        session_event = None
        in_reply_to = None

        if is_ussd:
            session = USSDSession.objects.get_with_status_only(
                msg.connection_id)
            if session and session.should_end:
                session_event = "close"
            else:
                session_event = "resume"

        if msg.response_to_id:
            in_reply_to = Msg.objects.values_list(
                'external_id',
                flat=True).filter(pk=msg.response_to_id).first()

        payload = dict(message_id=msg.id,
                       in_reply_to=in_reply_to,
                       session_event=session_event,
                       to_addr=msg.urn_path,
                       from_addr=channel.address,
                       content=text,
                       transport_name=channel.config['transport_name'],
                       transport_type='ussd' if is_ussd else 'sms',
                       transport_metadata={},
                       helper_metadata={})

        payload = json.dumps(payload)

        headers = http_headers(extra={'Content-Type': 'application/json'})

        api_url_base = channel.config.get('api_url', Channel.VUMI_GO_API_URL)

        url = "%s/%s/messages.json" % (api_url_base,
                                       channel.config['conversation_key'])

        event = HttpEvent('PUT', url, json.dumps(payload))

        start = time.time()

        validator = URLValidator()
        validator(url)

        try:
            response = requests.put(url,
                                    data=payload,
                                    headers=headers,
                                    timeout=30,
                                    auth=(channel.config['account_key'],
                                          channel.config['access_token']))

            event.status_code = response.status_code
            event.response_body = response.text

        except Exception as e:
            raise SendException(six.text_type(e), event=event, start=start)

        if response.status_code not in (200, 201):
            # this is a fatal failure, don't retry
            fatal = response.status_code == 400

            # if this is fatal due to the user opting out, stop them
            if response.text and response.text.find('has opted out') >= 0:
                contact = Contact.objects.get(id=msg.contact)
                contact.stop(contact.modified_by)
                fatal = True

            raise SendException("Got non-200 response [%d] from API" %
                                response.status_code,
                                event=event,
                                fatal=fatal,
                                start=start)

        # parse our response
        body = response.json()
        external_id = body.get('message_id', '')

        if is_ussd and session and session.should_end:
            session.close()

        Channel.success(channel,
                        msg,
                        WIRED,
                        start,
                        event=event,
                        external_id=external_id)