Esempio n. 1
0
    def setUp(self):
        self.clear_cache()

        self.user = self.create_user("tito")
        self.admin = self.create_user("Administrator")
        self.org = Org.objects.create(name="Nyaruka Ltd.", timezone="Africa/Kigali",
                                      created_by=self.user, modified_by=self.user)
        self.org.initialize()

        self.org.administrators.add(self.admin)
        self.admin.set_org(self.org)
        self.org.administrators.add(self.user)
        self.user.set_org(self.org)

        self.tel_mtn = Channel.create(self.org, self.user, 'RW', 'A', name="MTN", address="+250780000000",
                                      secret="12345", gcm_id="123")
        self.tel_tigo = Channel.create(self.org, self.user, 'RW', 'A', name="Tigo", address="+250720000000",
                                       secret="23456", gcm_id="234")
        self.tel_bulk = Channel.create(self.org, self.user, 'RW', 'NX', name="Nexmo", parent=self.tel_tigo)
        self.twitter = Channel.create(self.org, self.user, None, 'TT', name="Twitter", address="billy_bob")

        # for generating tuples of scheme, path and channel
        def generate_tel_mtn(num):
            return TEL_SCHEME, "+25078%07d" % (num + 1), self.tel_mtn

        def generate_tel_tigo(num):
            return TEL_SCHEME, "+25072%07d" % (num + 1), self.tel_tigo

        def generate_twitter(num):
            return TWITTER_SCHEME, "tweep_%d" % (num + 1), self.twitter

        self.urn_generators = (generate_tel_mtn, generate_tel_tigo, generate_twitter)

        self.field_nick = ContactField.get_or_create(self.org, self.admin, 'nick', 'Nickname', show_in_table=True, value_type=Value.TYPE_TEXT)
        self.field_age = ContactField.get_or_create(self.org, self.admin, 'age', 'Age', show_in_table=True, value_type=Value.TYPE_DECIMAL)
Esempio n. 2
0
    def test_refresh_channel_auth_tokens(self, patched_delay):

        # channel set for refreshing
        refresh_channel = Channel.create(
            self.org, self.user, 'RW', WhatsAppDirectType.code,
            None, '+27000000000',
            config={
                "authorization": {
                    "access_token": "a",
                    "refresh_token": "b",
                },
                "expires_at": datetime.now().isoformat(),
            },
            uuid='00000000-0000-0000-0000-000000001234',
            role=Channel.DEFAULT_ROLE)

        # channel still ok
        Channel.create(
            self.org, self.user, 'RW', WhatsAppDirectType.code,
            None, '+27000000000',
            config={
                "authorization": {
                    "access_token": "a",
                    "refresh_token": "b",
                },
                "expires_at": (
                    datetime.now() + timedelta(days=5)).isoformat(),
            },
            uuid='00000000-0000-0000-0000-000000005678',
            role=Channel.DEFAULT_ROLE)

        refresh_channel_auth_tokens()
        patched_delay.assert_called_with(refresh_channel.pk)
Esempio n. 3
0
    def send(self, channel, msg, text):

        client = NexmoClient(channel.org_config[NEXMO_KEY],
                             channel.org_config[NEXMO_SECRET],
                             channel.org_config[NEXMO_APP_ID],
                             channel.org_config[NEXMO_APP_PRIVATE_KEY])
        start = time.time()

        event = None
        attempts = 0
        while not event:
            try:
                (message_id, event) = client.send_message_via_nexmo(
                    channel.address, msg.urn_path, text)
            except SendException as e:
                match = regex.match(
                    r'.*Throughput Rate Exceeded - please wait \[ (\d+) \] and retry.*',
                    e.events[0].response_body)

                # this is a throughput failure, attempt to wait up to three times
                if match and attempts < 3:
                    sleep(float(match.group(1)) / 1000)
                    attempts += 1
                else:
                    raise e

        Channel.success(channel,
                        msg,
                        SENT,
                        start,
                        event=event,
                        external_id=message_id)
Esempio n. 4
0
    def test_api_request_headers(self):
        old_style_channel = Channel.create(
            self.org, self.user, 'RW', WhatsAppDirectType.code,
            None, '+27000000000',
            config=dict(api_token='api-token', secret='secret'),
            uuid='00000000-0000-0000-0000-000000001234',
            role=Channel.DEFAULT_ROLE)
        new_style_channel = Channel.create(
            self.org, self.user, 'RW', WhatsAppDirectType.code,
            None, '+27000000000',
            config={
                'authorization': {
                    'token_type': 'Bearer',
                    'access_token': 'foo',
                }
            },
            uuid='00000000-0000-0000-0000-000000005678',
            role=Channel.DEFAULT_ROLE)

        t = WhatsAppDirectType()
        self.assertEqual(
            t.api_request_headers(old_style_channel)['Authorization'],
            'Token api-token')
        self.assertEqual(
            t.api_request_headers(new_style_channel)['Authorization'],
            'Bearer foo')
Esempio n. 5
0
    def send(self, channel, msg, text):
        # url used for logs and exceptions
        url = 'https://api.plivo.com/v1/Account/%s/Message/' % channel.config[Channel.CONFIG_PLIVO_AUTH_ID]

        client = plivo.RestAPI(channel.config[Channel.CONFIG_PLIVO_AUTH_ID], channel.config[Channel.CONFIG_PLIVO_AUTH_TOKEN])
        status_url = "https://" + settings.TEMBA_HOST + "%s" % reverse('handlers.plivo_handler',
                                                                       args=['status', channel.uuid])

        payload = {'src': channel.address.lstrip('+'),
                   'dst': msg.urn_path.lstrip('+'),
                   'text': text,
                   'url': status_url,
                   'method': 'POST'}

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

        start = time.time()

        try:
            # TODO: Grab real request and response here
            plivo_response_status, plivo_response = client.send_message(params=payload)
            event.status_code = plivo_response_status
            event.response_body = plivo_response

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

        if plivo_response_status != 200 and plivo_response_status != 201 and plivo_response_status != 202:
            raise SendException("Got non-200 response [%d] from API" % plivo_response_status,
                                event=event, start=start)

        external_id = plivo_response['message_uuid'][0]
        Channel.success(channel, msg, WIRED, start, event=event, external_id=external_id)
Esempio n. 6
0
    def form_valid(self, form):
        org = self.request.user.get_org()
        authorization = self.get_session_authorization()
        access_token = authorization['access_token']

        group_uuid = form.cleaned_data['group']
        [group] = [
            group for group in self.get_groups(access_token)
            if group['uuid'] == group_uuid
        ]

        config = {
            'authorization':
            authorization,
            'expires_at':
            ((datetime.now() +
              timedelta(seconds=authorization['expires_in']))).isoformat(),
            'group':
            group,
        }

        self.object = Channel.create(
            org,
            self.request.user,
            None,
            self.channel_type,
            name='group messages to %(subject)s on %(number)s' % group,
            address=group['number'],
            config=config,
            secret=Channel.generate_secret())

        self.clear_session_authorization()
        return super(WhatsAppClaimView, self).form_valid(form)
Esempio n. 7
0
    def test_send_default_url(self):
        joe = self.create_contact("Joe", "+250788383383")
        self.create_group("Reporters", [joe])
        inbound = Msg.create_incoming(self.channel,
                                      "tel:+250788383383",
                                      "Send an inbound message",
                                      external_id='vumi-message-id')
        msg = inbound.reply("Test message", self.admin, trigger_send=False)

        # our outgoing message
        msg.refresh_from_db()

        try:
            settings.SEND_MESSAGES = True

            with patch('requests.put') as mock:
                mock.return_value = MockResponse(200,
                                                 '{ "message_id": "1515" }')

                # manually send it off
                Channel.send_message(
                    dict_to_struct('MsgStruct', msg.as_task_json()))

                self.assertEqual(
                    mock.call_args[0][0],
                    'https://go.vumi.org/api/v1/go/http_api_nostream/key/messages.json'
                )

                self.clear_cache()

        finally:
            settings.SEND_MESSAGES = False
Esempio n. 8
0
    def form_valid(self, form):
        org = self.request.user.get_org()
        number = form.cleaned_data['number']
        authorization = self.get_session_authorization()

        config = {
            'authorization':
            authorization,
            'expires_at':
            (datetime.now() +
             timedelta(seconds=authorization['expires_in'])).isoformat(),
            'number':
            number,
        }

        self.object = Channel.create(org,
                                     self.request.user,
                                     None,
                                     self.channel_type,
                                     name='Direct Messages to %s' % (number, ),
                                     address=number,
                                     config=config,
                                     secret=Channel.generate_secret())

        self.clear_session_authorization
        return super(WhatsAppClaimView, self).form_valid(form)
Esempio n. 9
0
    def test_ack(self):
        joe = self.create_contact("Joe", "+250788383383")
        self.create_group("Reporters", [joe])
        inbound = Msg.create_incoming(self.channel,
                                      "tel:+250788383383",
                                      "Send an inbound message",
                                      external_id='vumi-message-id',
                                      msg_type=USSD)
        msg = inbound.reply("Test message", self.admin, trigger_send=False)[0]

        # our outgoing message
        msg.refresh_from_db()

        try:
            settings.SEND_MESSAGES = True

            with patch('requests.put') as mock:
                mock.return_value = MockResponse(200,
                                                 '{ "message_id": "1515" }')

                # manually send it off
                Channel.send_message(
                    dict_to_struct('MsgStruct', msg.as_task_json()))

                # check the status of the message is now sent
                msg.refresh_from_db()
                self.assertEquals(WIRED, msg.status)
                self.assertTrue(msg.sent_on)
                self.assertEquals("1515", msg.external_id)
                self.assertEquals(1, mock.call_count)

                # simulate Vumi calling back to us sending an ACK event
                data = {
                    "transport_name": "ussd_transport",
                    "event_type": "ack",
                    "event_id": six.text_type(uuid.uuid4()),
                    "sent_message_id": six.text_type(uuid.uuid4()),
                    "helper_metadata": {},
                    "routing_metadata": {},
                    "message_version": "20110921",
                    "timestamp": six.text_type(timezone.now()),
                    "transport_metadata": {},
                    "user_message_id": msg.external_id,
                    "message_type": "event"
                }
                callback_url = reverse('handlers.vumi_handler',
                                       args=['event', self.channel.uuid])
                self.client.post(callback_url,
                                 json.dumps(data),
                                 content_type="application/json")

                # it should be SENT now
                msg.refresh_from_db()
                self.assertEquals(SENT, msg.status)

                self.clear_cache()
        finally:
            settings.SEND_MESSAGES = False
Esempio n. 10
0
    def test_nack(self):
        joe = self.create_contact("Joe", "+250788383383")
        self.create_group("Reporters", [joe])
        msg = joe.send("Test message", self.admin, trigger_send=False)

        # our outgoing message
        msg.refresh_from_db()
        r = get_redis_connection()

        try:
            settings.SEND_MESSAGES = True

            with patch('requests.put') as mock:
                mock.return_value = MockResponse(200,
                                                 '{ "message_id": "1515" }')

                # manually send it off
                Channel.send_message(
                    dict_to_struct('MsgStruct', msg.as_task_json()))

                # check the status of the message is now sent
                msg.refresh_from_db()
                self.assertEquals(WIRED, msg.status)
                self.assertTrue(msg.sent_on)
                self.assertEquals("1515", msg.external_id)
                self.assertEquals(1, mock.call_count)

                # should have a failsafe that it was sent
                self.assertTrue(
                    r.sismember(timezone.now().strftime(MSG_SENT_KEY),
                                str(msg.id)))

                # simulate Vumi calling back to us sending an NACK event
                data = {
                    "transport_name": "ussd_transport",
                    "event_type": "nack",
                    "nack_reason": "Unknown address.",
                    "event_id": six.text_type(uuid.uuid4()),
                    "timestamp": six.text_type(timezone.now()),
                    "message_version": "20110921",
                    "transport_metadata": {},
                    "user_message_id": msg.external_id,
                    "message_type": "event"
                }
                callback_url = reverse('handlers.vumi_handler',
                                       args=['event', self.channel.uuid])
                response = self.client.post(callback_url,
                                            json.dumps(data),
                                            content_type="application/json")

                self.assertEqual(response.status_code, 200)
                self.assertTrue(
                    self.create_contact("Joe", "+250788383383").is_stopped)

                self.clear_cache()
        finally:
            settings.SEND_MESSAGES = False
Esempio n. 11
0
    def send_whatsapp(self, channel_struct, msg, payload, attachments=None):
        url = ('%s/messages/' % (self.wassup_url(), ))
        headers = self.api_request_headers(channel_struct)
        event = HttpEvent('POST', url, json.dumps(payload))
        start = time.time()

        # Grab the first attachment if it exists
        attachments = Attachment.parse_all(msg.attachments)
        attachment = attachments[0] if attachments else None

        try:
            if attachment:
                files = self.fetch_attachment(attachment)
                data = payload
            else:
                headers.update({'Content-Type': 'application/json'})
                data = json.dumps(payload)
                files = {}

            response = requests.post(url,
                                     data=data,
                                     files=files,
                                     headers=headers)
            response.raise_for_status()
            event.status_code = response.status_code
            event.response_body = response.text
        except (requests.RequestException, ) as e:
            raise SendException(
                'error: %s, request: %r, response: %r' %
                (six.text_type(e), e.request.body, e.response.content),
                event=event,
                start=start)

        data = response.json()
        try:
            message_id = data['uuid']
            Channel.success(channel_struct,
                            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',
                                url,
                                request_body=json.dumps(json.dumps(payload)),
                                response_body=json.dumps(data)),
                start=start)
Esempio n. 12
0
    def test_send(self):
        joe = self.create_contact("Joe", "+250788383383")
        self.create_group("Reporters", [joe])
        inbound = Msg.create_incoming(self.channel,
                                      "tel:+250788383383",
                                      "Send an inbound message",
                                      external_id='vumi-message-id',
                                      msg_type=USSD)
        msg = inbound.reply("Test message", self.admin, trigger_send=False)[0]
        self.assertEqual(inbound.msg_type, USSD)
        self.assertEqual(msg.msg_type, USSD)

        # our outgoing message
        msg.refresh_from_db()
        r = get_redis_connection()

        try:
            settings.SEND_MESSAGES = True

            with patch('requests.put') as mock:
                mock.return_value = MockResponse(200,
                                                 '{ "message_id": "1515" }')

                # manually send it off
                Channel.send_message(
                    dict_to_struct('MsgStruct', msg.as_task_json()))

                # check the status of the message is now sent
                msg.refresh_from_db()
                self.assertEquals(WIRED, msg.status)
                self.assertTrue(msg.sent_on)
                self.assertEquals("1515", msg.external_id)
                self.assertEquals(1, mock.call_count)

                # should have a failsafe that it was sent
                self.assertTrue(
                    r.sismember(timezone.now().strftime(MSG_SENT_KEY),
                                str(msg.id)))

                # try sending again, our failsafe should kick in
                Channel.send_message(
                    dict_to_struct('MsgStruct', msg.as_task_json()))

                # we shouldn't have been called again
                self.assertEquals(1, mock.call_count)

                self.clear_cache()
        finally:
            settings.SEND_MESSAGES = False
Esempio n. 13
0
    def setUp(self):
        super(APITest, self).setUp()

        self.joe = self.create_contact("Joe Blow", "0788123123")
        self.frank = self.create_contact("Frank", twitter="franky")
        self.test_contact = Contact.get_test_contact(self.user)

        self.twitter = Channel.create(self.org,
                                      self.user,
                                      None,
                                      'TT',
                                      name="Twitter Channel",
                                      address="billy_bob",
                                      role="SR",
                                      scheme='twitter')

        self.create_secondary_org()
        self.hans = self.create_contact("Hans Gruber",
                                        "+4921551511",
                                        org=self.org2)

        self.maxDiff = None

        # this is needed to prevent REST framework from rolling back transaction created around each unit test
        connection.settings_dict['ATOMIC_REQUESTS'] = False
Esempio n. 14
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()
Esempio n. 15
0
    def create(cls,
               org,
               user,
               courier_url,
               callback,
               country="EC",
               scheme="tel",
               address="123456",
               port=49999):
        server = cls.Server(port)

        config = {
            Channel.CONFIG_SEND_URL: f"{server.base_url}/send",
            Channel.CONFIG_SEND_METHOD: "POST",
            Channel.CONFIG_CONTENT_TYPE: "application/json",
            Channel.CONFIG_SEND_BODY: '{"text": "{{text}}"}',
        }

        db_channel = Channel.add_config_external_channel(org,
                                                         user,
                                                         country,
                                                         address,
                                                         "EX",
                                                         config,
                                                         "SR", [scheme],
                                                         name="Test Channel")

        return cls(db_channel, server, courier_url, callback)
Esempio n. 16
0
    def __init__(self, user, *args, **kwargs):
        flows = Flow.objects.filter(
            org=user.get_org(), is_active=True, is_archived=False, flow_type__in=[Flow.TYPE_USSD]
        )
        super().__init__(user, flows, *args, **kwargs)

        self.fields["channel"].queryset = Channel.get_by_category(self.user.get_org(), ChannelType.Category.USSD)
Esempio n. 17
0
    def test_range_details(self):

        url = reverse('dashboard.dashboard_range_details')

        # visit this page without authenticating
        response = self.client.get(url, follow=True)

        # nope!
        self.assertRedirects(response, "/users/login/?next=%s" % url)

        self.login(self.admin)
        self.create_activity()

        types = ['T', 'TT', 'FB', 'NX', 'AT', 'KN', 'CK']
        michael = self.create_contact("Michael", twitter="mjackson")
        for t in types:
            channel = Channel.create(self.org, self.user, None, t, name="Test Channel %s" % t, address="%s:1234" % t)
            self.create_msg(contact=michael, direction='O', text="Message on %s" % t, channel=channel)
        response = self.client.get(url)

        # org message activity
        self.assertEqual(12, response.context['orgs'][0]['count_sum'])
        self.assertEqual('Temba', response.context['orgs'][0]['channel__org__name'])

        # our pie chart
        self.assertEqual(5, response.context['channel_types'][0]['count_sum'])
        self.assertEqual('Android', response.context['channel_types'][0]['channel__name'])
        self.assertEqual(7, len(response.context['channel_types']))
        self.assertEqual('Other', response.context['channel_types'][6]['channel__name'])
Esempio n. 18
0
    def setUp(self):
        super(JunebugUSSDTest, self).setUp()

        flow = self.get_flow('ussd_example')
        self.starcode = "*113#"

        self.channel.delete()
        self.channel = Channel.create(
            self.org,
            self.user,
            'RW',
            Channel.TYPE_JUNEBUG_USSD,
            None,
            '1234',
            config=dict(username='******',
                        password='******',
                        send_url='http://example.org/'),
            uuid='00000000-0000-0000-0000-000000001234',
            role=Channel.ROLE_USSD)

        self.trigger, _ = Trigger.objects.get_or_create(
            channel=self.channel,
            keyword=self.starcode,
            flow=flow,
            created_by=self.user,
            modified_by=self.user,
            org=self.org,
            trigger_type=Trigger.TYPE_USSD_PULL)
Esempio n. 19
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,
        )
Esempio n. 20
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()
Esempio n. 21
0
    def test_add_contact_changed(self):
        twitter = Channel.create(
            self.org, self.admin, None, "TT", "Twitter", "nyaruka", schemes=["twitter", "twitterid"]
        )
        self.contact.set_preferred_channel(twitter)
        self.contact.urns.filter(scheme="twitterid").update(channel=twitter)
        self.contact.clear_urn_cache()

        with patch("django.utils.timezone.now", return_value=datetime(2018, 1, 18, 14, 24, 30, 0, tzinfo=pytz.UTC)):
            self.contact.set_field(self.admin, "gender", "M")
            self.contact.set_field(self.admin, "age", 36)

            self.assertEqual(
                self.client.request_builder(self.org).add_contact_changed(self.contact).request["events"],
                [
                    {
                        "type": "contact_changed",
                        "created_on": "2018-01-18T14:24:30+00:00",
                        "contact": {
                            "uuid": str(self.contact.uuid),
                            "id": self.contact.id,
                            "name": "Bob",
                            "language": None,
                            "urns": [
                                "twitterid:123456785?channel=%s#bobby" % str(twitter.uuid),
                                "tel:+12345670987?channel=%s" % str(self.channel.uuid),
                            ],
                            "fields": {"gender": {"text": "M"}, "age": {"text": "36", "number": "36"}},
                            "groups": [{"uuid": str(self.testers.uuid), "name": "Testers"}],
                        },
                    }
                ],
            )
Esempio n. 22
0
    def setUp(self):
        super().setUp()

        flow = self.get_flow("ussd_example")
        self.starcode = "*113#"

        self.channel.delete()
        self.channel = Channel.create(
            self.org,
            self.user,
            "RW",
            "JNU",
            None,
            "1234",
            config=dict(username="******", password="******", send_url="http://example.org/"),
            uuid="00000000-0000-0000-0000-000000001234",
            role=Channel.ROLE_USSD,
        )

        self.trigger, _ = Trigger.objects.get_or_create(
            channel=self.channel,
            keyword=self.starcode,
            flow=flow,
            created_by=self.user,
            modified_by=self.user,
            org=self.org,
            trigger_type=Trigger.TYPE_USSD_PULL,
        )
Esempio n. 23
0
    def setUp(self):
        super().setUp()

        flow = self.get_flow("ussd_example")
        self.starcode = "*113#"

        self.channel.delete()
        self.channel = Channel.create(
            self.org,
            self.user,
            "RW",
            "JNU",
            None,
            "1234",
            config=dict(username="******",
                        password="******",
                        send_url="http://example.org/"),
            uuid="00000000-0000-0000-0000-000000001234",
            role=Channel.ROLE_USSD,
        )

        self.trigger, _ = Trigger.objects.get_or_create(
            channel=self.channel,
            keyword=self.starcode,
            flow=flow,
            created_by=self.user,
            modified_by=self.user,
            org=self.org,
            trigger_type=Trigger.TYPE_USSD_PULL,
        )
Esempio n. 24
0
    def setUp(self):
        super(USSDSessionTest, self).setUp()

        self.channel.delete()
        self.channel = Channel.create(self.org, self.user, 'RW', 'JNU', None, '+250788123123',
                                      role=Channel.ROLE_USSD + Channel.DEFAULT_ROLE,
                                      uuid='00000000-0000-0000-0000-000000001234')
Esempio n. 25
0
    def __init__(self, user, *args, **kwargs):
        flows = Flow.objects.filter(
            org=user.get_org(), is_active=True, is_archived=False, flow_type__in=[Flow.TYPE_USSD]
        )
        super().__init__(user, flows, *args, **kwargs)

        self.fields["channel"].queryset = Channel.get_by_category(self.user.get_org(), ChannelType.Category.USSD)
Esempio n. 26
0
    def send(self, channel, msg, text):
        auth_id = channel.config[Channel.CONFIG_PLIVO_AUTH_ID]
        auth_token = channel.config[Channel.CONFIG_PLIVO_AUTH_TOKEN]

        url = 'https://api.plivo.com/v1/Account/%s/Message/' % auth_id
        status_url = "https://%s%s" % (channel.callback_domain,
                                       reverse('handlers.plivo_handler',
                                               args=['status', channel.uuid]))

        payload = {
            'src': channel.address.lstrip('+'),
            'dst': msg.urn_path.lstrip('+'),
            'text': text,
            'url': status_url,
            'method': 'POST'
        }

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

        start = time.time()

        try:
            # TODO: Grab real request and response here
            response = requests.post(url,
                                     json=payload,
                                     headers=headers,
                                     auth=(auth_id, auth_token))
            event.status_code = response.status_code
            event.response_body = response.json()

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

        if response.status_code not in [200, 201, 202]:  # pragma: no cover
            raise SendException("Got non-200 response [%d] from API" %
                                response.status_code,
                                event=event,
                                start=start)

        external_id = response.json()['message_uuid'][0]
        Channel.success(channel,
                        msg,
                        WIRED,
                        start,
                        event=event,
                        external_id=external_id)
Esempio n. 27
0
 def setUp(self):
     super(WhatsAppDirectTypeTest, self).setUp()
     self.channel = Channel.create(
         self.org, self.user, 'RW', WhatsAppDirectType.code,
         None, '+27000000000',
         config=dict(api_token='api-token', secret='secret'),
         uuid='00000000-0000-0000-0000-000000001234',
         role=Channel.DEFAULT_ROLE)
Esempio n. 28
0
    def setUp(self):

        # if we are super verbose, turn on debug for sql queries
        if self.get_verbosity() > 2:
            settings.DEBUG = True

        self.clear_cache()

        self.superuser = User.objects.create_superuser(username="******", email="*****@*****.**", password="******")

        # create different user types
        self.non_org_user = self.create_user("NonOrg")
        self.user = self.create_user("User")
        self.editor = self.create_user("Editor")
        self.admin = self.create_user("Administrator")
        self.surveyor = self.create_user("Surveyor")

        # setup admin boundaries for Rwanda
        self.country = AdminBoundary.objects.create(osm_id='171496', name='Rwanda', level=0)
        self.state1 = AdminBoundary.objects.create(osm_id='1708283', name='Kigali City', level=1, parent=self.country)
        self.state2 = AdminBoundary.objects.create(osm_id='171591', name='Eastern Province', level=1, parent=self.country)
        self.district1 = AdminBoundary.objects.create(osm_id='1711131', name='Gatsibo', level=2, parent=self.state2)
        self.district2 = AdminBoundary.objects.create(osm_id='1711163', name='Kayônza', level=2, parent=self.state2)
        self.district3 = AdminBoundary.objects.create(osm_id='3963734', name='Nyarugenge', level=2, parent=self.state1)
        self.district4 = AdminBoundary.objects.create(osm_id='1711142', name='Rwamagana', level=2, parent=self.state2)
        self.ward1 = AdminBoundary.objects.create(osm_id='171113181', name='Kageyo', level=3, parent=self.district1)
        self.ward2 = AdminBoundary.objects.create(osm_id='171116381', name='Kabare', level=3, parent=self.district2)
        self.ward3 = AdminBoundary.objects.create(osm_id='171114281', name='Bukure', level=3, parent=self.district4)

        self.org = Org.objects.create(name="Temba", timezone="Africa/Kigali", country=self.country, brand=settings.DEFAULT_BRAND,
                                      created_by=self.user, modified_by=self.user)

        self.org.initialize(topup_size=1000)

        # add users to the org
        self.user.set_org(self.org)
        self.org.viewers.add(self.user)

        self.editor.set_org(self.org)
        self.org.editors.add(self.editor)

        self.admin.set_org(self.org)
        self.org.administrators.add(self.admin)

        self.surveyor.set_org(self.org)
        self.org.surveyors.add(self.surveyor)

        self.superuser.set_org(self.org)

        # welcome topup with 1000 credits
        self.welcome_topup = self.org.topups.all()[0]

        # a single Android channel
        self.channel = Channel.create(self.org, self.user, 'RW', 'A', name="Test Channel", address="+250785551212",
                                      device="Nexus 5X", secret="12345", gcm_id="123")

        # reset our simulation to False
        Contact.set_simulation(False)
Esempio n. 29
0
    def setUp(self):

        # if we are super verbose, turn on debug for sql queries
        if self.get_verbosity() > 2:
            settings.DEBUG = True

        self.clear_cache()

        self.superuser = User.objects.create_superuser(username="******", email="*****@*****.**", password="******")

        # create different user types
        self.non_org_user = self.create_user("NonOrg")
        self.user = self.create_user("User")
        self.editor = self.create_user("Editor")
        self.admin = self.create_user("Administrator")
        self.surveyor = self.create_user("Surveyor")

        # setup admin boundaries for Rwanda
        self.country = AdminBoundary.objects.create(osm_id='171496', name='Rwanda', level=0)
        self.state1 = AdminBoundary.objects.create(osm_id='1708283', name='Kigali City', level=1, parent=self.country)
        self.state2 = AdminBoundary.objects.create(osm_id='171591', name='Eastern Province', level=1, parent=self.country)
        self.district1 = AdminBoundary.objects.create(osm_id='1711131', name='Gatsibo', level=2, parent=self.state2)
        self.district2 = AdminBoundary.objects.create(osm_id='1711163', name='Kayônza', level=2, parent=self.state2)
        self.district3 = AdminBoundary.objects.create(osm_id='3963734', name='Nyarugenge', level=2, parent=self.state1)
        self.district4 = AdminBoundary.objects.create(osm_id='1711142', name='Rwamagana', level=2, parent=self.state2)
        self.ward1 = AdminBoundary.objects.create(osm_id='171113181', name='Kageyo', level=3, parent=self.district1)
        self.ward2 = AdminBoundary.objects.create(osm_id='171116381', name='Kabare', level=3, parent=self.district2)
        self.ward3 = AdminBoundary.objects.create(osm_id='171114281', name='Bukure', level=3, parent=self.district4)

        self.org = Org.objects.create(name="Temba", timezone=pytz.timezone("Africa/Kigali"), country=self.country,
                                      brand=settings.DEFAULT_BRAND, created_by=self.user, modified_by=self.user)

        self.org.initialize(topup_size=1000)

        # add users to the org
        self.user.set_org(self.org)
        self.org.viewers.add(self.user)

        self.editor.set_org(self.org)
        self.org.editors.add(self.editor)

        self.admin.set_org(self.org)
        self.org.administrators.add(self.admin)

        self.surveyor.set_org(self.org)
        self.org.surveyors.add(self.surveyor)

        self.superuser.set_org(self.org)

        # welcome topup with 1000 credits
        self.welcome_topup = self.org.topups.all()[0]

        # a single Android channel
        self.channel = Channel.create(self.org, self.user, 'RW', 'A', name="Test Channel", address="+250785551212",
                                      device="Nexus 5X", secret="12345", gcm_id="123")

        # reset our simulation to False
        Contact.set_simulation(False)
Esempio n. 30
0
    def claim_number(self, user, phone_number, country, role):

        auth_id = self.request.session.get(Channel.CONFIG_PLIVO_AUTH_ID, None)
        auth_token = self.request.session.get(Channel.CONFIG_PLIVO_AUTH_TOKEN, None)

        org = user.get_org()

        plivo_uuid = generate_uuid()
        callback_domain = org.get_brand_domain()
        app_name = "%s/%s" % (callback_domain.lower(), plivo_uuid)

        message_url = "https://" + callback_domain + "%s" % reverse('handlers.plivo_handler', args=['receive', plivo_uuid])
        answer_url = "https://" + settings.AWS_BUCKET_DOMAIN + "/plivo_voice_unavailable.xml"

        headers = http_headers(extra={'Content-Type': "application/json"})
        create_app_url = "https://api.plivo.com/v1/Account/%s/Application/" % auth_id

        response = requests.post(create_app_url, json=dict(app_name=app_name, answer_url=answer_url, message_url=message_url),
                                 headers=headers, auth=(auth_id, auth_token))

        if response.status_code in [201, 200, 202]:
            plivo_app_id = response.json()['app_id']
        else:  # pragma: no cover
            plivo_app_id = None

        plivo_config = {Channel.CONFIG_PLIVO_AUTH_ID: auth_id,
                        Channel.CONFIG_PLIVO_AUTH_TOKEN: auth_token,
                        Channel.CONFIG_PLIVO_APP_ID: plivo_app_id,
                        Channel.CONFIG_CALLBACK_DOMAIN: org.get_brand_domain()}

        plivo_number = phone_number.strip('+ ').replace(' ', '')
        response = requests.get("https://api.plivo.com/v1/Account/%s/Number/%s/" % (auth_id, plivo_number), headers=headers, auth=(auth_id, auth_token))

        if response.status_code != 200:
            response = requests.post("https://api.plivo.com/v1/Account/%s/PhoneNumber/%s/" % (auth_id, plivo_number), headers=headers, auth=(auth_id, auth_token))

            if response.status_code != 201:  # pragma: no cover
                raise Exception(_("There was a problem claiming that number, please check the balance on your account."))

            response = requests.get("https://api.plivo.com/v1/Account/%s/Number/%s/" % (auth_id, plivo_number), headers=headers, auth=(auth_id, auth_token))

        if response.status_code == 200:
            response = requests.post("https://api.plivo.com/v1/Account/%s/Number/%s/" % (auth_id, plivo_number),
                                     json=dict(app_id=plivo_app_id), headers=headers, auth=(auth_id, auth_token))

            if response.status_code != 202:  # pragma: no cover
                raise Exception(_("There was a problem updating that number, please try again."))

        phone_number = '+' + plivo_number
        phone = phonenumbers.format_number(phonenumbers.parse(phone_number, None),
                                           phonenumbers.PhoneNumberFormat.NATIONAL)

        channel = Channel.create(org, user, country, 'PL', name=phone, address=phone_number,
                                 config=plivo_config, uuid=plivo_uuid)

        analytics.track(user.username, 'temba.channel_claim_plivo', dict(number=phone_number))

        return channel
Esempio n. 31
0
    def test_channel(self):
        channel = Channel.add_config_external_channel(self.org, self.admin,
                                                      "US", "+12061111111",
                                                      "KN", {})

        action = SetChannelAction(str(uuid4()), channel)
        action = self._serialize_deserialize(action)

        self.assertEqual(channel, action.channel)
Esempio n. 32
0
    def register_active_event(self):
        """
        Helper function for registering active events on a throttled channel
        """
        r = get_redis_connection()

        channel_key = Channel.redis_active_events_key(self.channel_id)

        r.incr(channel_key)
Esempio n. 33
0
    def setUp(self):
        super(VumiUssdTest, self).setUp()

        self.channel.delete()
        self.channel = Channel.create(self.org, self.user, 'RW', 'VMU', None, '+250788123123',
                                      config=dict(account_key='vumi-key', access_token='vumi-token',
                                                  conversation_key='key'),
                                      uuid='00000000-0000-0000-0000-000000001234',
                                      role=Channel.ROLE_USSD)
Esempio n. 34
0
    def browser(self):

        self.driver.set_window_size(1024, 2000)

        # view the homepage
        self.fetch_page()

        # go directly to our signup
        self.fetch_page(reverse("orgs.org_signup"))

        # create account
        self.keys("email", "*****@*****.**")
        self.keys("password", "SuperSafe1")
        self.keys("first_name", "Joe")
        self.keys("last_name", "Blow")
        self.click("#form-one-submit")
        self.keys("name", "Temba")
        self.click("#form-two-submit")

        # set up our channel for claiming
        channel = Channel.create(
            None,
            get_anonymous_user(),
            "RW",
            "A",
            name="Test Channel",
            address="0785551212",
            claim_code="AAABBBCCC",
            secret="12345",
            gcm_id="123",
        )

        # and claim it
        self.fetch_page(reverse("channels.channel_claim_android"))
        self.keys("#id_claim_code", "AAABBBCCC")
        self.keys("#id_phone_number", "0785551212")
        self.submit(".claim-form")

        # get our freshly claimed channel
        channel = Channel.objects.get(pk=channel.pk)

        # now go to the contacts page
        self.click("#menu-right .icon-contact")
        self.click("#id_import_contacts")

        # upload some contacts
        directory = os.path.dirname(os.path.realpath(__file__))
        self.keys("#csv_file",
                  "%s/../media/test_imports/sample_contacts.xls" % directory)
        self.submit(".smartmin-form")

        # make sure they are there
        self.click("#menu-right .icon-contact")
        self.assertInElements(".value-phone", "+250788382382")
        self.assertInElements(".value-text", "Eric Newcomer")
        self.assertInElements(".value-text", "Sample Contacts")
Esempio n. 35
0
    def browser(self):

        self.driver.set_window_size(1024, 2000)

        # view the homepage
        self.fetch_page()

        # go directly to our signup
        self.fetch_page(reverse("orgs.org_signup"))

        # create account
        self.keys("email", "*****@*****.**")
        self.keys("password", "SuperSafe1")
        self.keys("first_name", "Joe")
        self.keys("last_name", "Blow")
        self.click("#form-one-submit")
        self.keys("name", "Temba")
        self.click("#form-two-submit")

        # set up our channel for claiming
        channel = Channel.create(
            None,
            get_anonymous_user(),
            "RW",
            "A",
            name="Test Channel",
            address="0785551212",
            claim_code="AAABBBCCC",
            secret="12345",
            gcm_id="123",
        )

        # and claim it
        self.fetch_page(reverse("channels.channel_claim_android"))
        self.keys("#id_claim_code", "AAABBBCCC")
        self.keys("#id_phone_number", "0785551212")
        self.submit(".claim-form")

        # get our freshly claimed channel
        channel = Channel.objects.get(pk=channel.pk)

        # now go to the contacts page
        self.click("#menu-right .icon-contact")
        self.click("#id_import_contacts")

        # upload some contacts
        directory = os.path.dirname(os.path.realpath(__file__))
        self.keys("#csv_file", "%s/../media/test_imports/sample_contacts.xls" % directory)
        self.submit(".smartmin-form")

        # make sure they are there
        self.click("#menu-right .icon-contact")
        self.assertInElements(".value-phone", "+250788382382")
        self.assertInElements(".value-text", "Eric Newcomer")
        self.assertInElements(".value-text", "Sample Contacts")
Esempio n. 36
0
    def __init__(self, user, *args, **kwargs):
        flows = Flow.objects.filter(is_archived=False, org=user.get_org(), flow_type__in=[Flow.FLOW, Flow.VOICE])
        super(FollowTriggerForm, self).__init__(user, flows, *args, **kwargs)

        # all channel types that support follow triggers
        types_for_follow = set()
        for scheme in URN_SCHEMES_SUPPORTING_FOLLOW:
            types_for_follow.update(Channel.types_for_scheme(scheme))

        self.fields['channel'].queryset = Channel.objects.filter(is_active=True, org=self.user.get_org(),
                                                                 channel_type__in=types_for_follow)
Esempio n. 37
0
    def form_valid(self, form):
        org = self.request.user.get_org()

        if not org:  # pragma: no cover
            raise Exception(_("No org for this user, cannot claim"))

        data = form.cleaned_data
        self.object = Channel.add_config_external_channel(
            org, self.request.user, data["country"], data["number"], "CT", {Channel.CONFIG_API_KEY: data["api_key"]}
        )

        return super(AuthenticatedExternalClaimView, self).form_valid(form)
Esempio n. 38
0
    def test_channels(self):
        url = reverse('api.v2.channels')

        self.assertEndpointAccess(url)

        # create channel for other org
        Channel.create(self.org2, self.admin2, None, 'TT', name="Twitter Channel",
                       address="nyaruka", role="SR", scheme='twitter')

        # no filtering
        with self.assertNumQueries(NUM_BASE_REQUEST_QUERIES + 2):
            response = self.fetchJSON(url)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json['next'], None)
        self.assertResultsByUUID(response, [self.twitter, self.channel])
        self.assertEqual(response.json['results'][1], {
            'uuid': self.channel.uuid,
            'name': "Test Channel",
            'address': "+250785551212",
            'country': "RW",
            'device': {
                'name': "Nexus 5X",
                'network_type': None,
                'power_level': -1,
                'power_source': None,
                'power_status': None
            },
            'last_seen': format_datetime(self.channel.last_seen),
            'created_on': format_datetime(self.channel.created_on)
        })

        # filter by UUID
        response = self.fetchJSON(url, 'uuid=%s' % self.twitter.uuid)
        self.assertResultsByUUID(response, [self.twitter])

        # filter by address
        response = self.fetchJSON(url, 'address=billy_bob')
        self.assertResultsByUUID(response, [self.twitter])
Esempio n. 39
0
    def setUp(self):
        super().setUp()

        self.channel.delete()
        self.channel = Channel.create(
            self.org,
            self.user,
            "RW",
            "JNU",
            None,
            "+250788123123",
            role=Channel.ROLE_USSD + Channel.DEFAULT_ROLE,
            uuid="00000000-0000-0000-0000-000000001234",
        )
Esempio n. 40
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()
Esempio n. 41
0
    def setUp(self):
        self.clear_cache()

        self.superuser = User.objects.create_superuser(username="******", email="*****@*****.**", password="******")

        # create different user types
        self.non_org_user = self.create_user("NonOrg")
        self.user = self.create_user("User")
        self.editor = self.create_user("Editor")
        self.admin = self.create_user("Administrator")
        self.surveyor = self.create_user("Surveyor")

        # setup admin boundaries for Rwanda
        self.country = AdminBoundary.objects.create(osm_id='171496', name='Rwanda', level=0)
        self.state1 = AdminBoundary.objects.create(osm_id='1708283', name='Kigali City', level=1, parent=self.country)
        self.state2 = AdminBoundary.objects.create(osm_id='171591', name='Eastern Province', level=1, parent=self.country)
        self.district1 = AdminBoundary.objects.create(osm_id='1711131', name='Gatsibo', level=2, parent=self.state2)
        self.district2 = AdminBoundary.objects.create(osm_id='1711163', name='Kayonza', level=2, parent=self.state2)
        self.district3 = AdminBoundary.objects.create(osm_id='60485579', name='Kigali', level=2, parent=self.state1)
        self.district4 = AdminBoundary.objects.create(osm_id='1711142', name='Rwamagana', level=2, parent=self.state2)

        self.org = Org.objects.create(name="Temba", timezone="Africa/Kigali", country=self.country,
                                      created_by=self.user, modified_by=self.user)
        self.org.initialize()

        # add users to the org
        self.user.set_org(self.org)
        self.org.viewers.add(self.user)

        self.editor.set_org(self.org)
        self.org.editors.add(self.editor)

        self.admin.set_org(self.org)
        self.org.administrators.add(self.admin)

        self.surveyor.set_org(self.org)
        self.org.surveyors.add(self.surveyor)

        self.superuser.set_org(self.org)

        # welcome topup with 1000 credits
        self.welcome_topup = self.org.topups.all()[0]

        # a single Android channel
        self.channel = Channel.create(self.org, self.user, 'RW', 'A', name="Test Channel", address="+250785551212",
                                      secret="12345", gcm_id="123")

        # reset our simulation to False
        Contact.set_simulation(False)
Esempio n. 42
0
    def setUp(self):
        self.clear_cache()

        self.user = self.create_user("tito")
        self.admin = self.create_user("Administrator")
        self.org = Org.objects.create(
            name="Nyaruka Ltd.", timezone="Africa/Kigali", created_by=self.user, modified_by=self.user
        )
        self.org.initialize()

        self.org.administrators.add(self.admin)
        self.admin.set_org(self.org)
        self.org.administrators.add(self.user)
        self.user.set_org(self.org)

        self.tel_mtn = Channel.create(
            self.org, self.user, "RW", "A", name="MTN", address="+250780000000", secret="12345", gcm_id="123"
        )
        self.tel_tigo = Channel.create(
            self.org, self.user, "RW", "A", name="Tigo", address="+250720000000", secret="23456", gcm_id="234"
        )
        self.tel_bulk = Channel.create(self.org, self.user, "RW", "NX", name="Nexmo", parent=self.tel_tigo)
        self.twitter = Channel.create(self.org, self.user, None, "TT", name="Twitter", address="billy_bob")

        # for generating tuples of scheme, path and channel
        generate_tel_mtn = lambda num: (TEL_SCHEME, "+25078%07d" % (num + 1), self.tel_mtn)
        generate_tel_tigo = lambda num: (TEL_SCHEME, "+25072%07d" % (num + 1), self.tel_tigo)
        generate_twitter = lambda num: (TWITTER_SCHEME, "tweep_%d" % (num + 1), self.twitter)
        self.urn_generators = (generate_tel_mtn, generate_tel_tigo, generate_twitter)

        self.field_nick = ContactField.get_or_create(
            self.org, self.admin, "nick", "Nickname", show_in_table=True, value_type=TEXT
        )
        self.field_age = ContactField.get_or_create(
            self.org, self.admin, "age", "Age", show_in_table=True, value_type=DECIMAL
        )
Esempio n. 43
0
    def browser(self):

        self.driver.set_window_size(1024, 2000)

        # view the homepage
        self.fetch_page()

        # go directly to our signup
        self.fetch_page(reverse('orgs.org_signup'))

        # create account
        self.keys('email', '*****@*****.**')
        self.keys('password', 'SuperSafe1')
        self.keys('first_name', 'Joe')
        self.keys('last_name', 'Blow')
        self.click('#form-one-submit')
        self.keys('name', 'Temba')
        self.click('#form-two-submit')

        # set up our channel for claiming
        anon = User.objects.get(pk=settings.ANONYMOUS_USER_ID)
        channel = Channel.create(None, anon, 'RW', 'A', name="Test Channel", address="0785551212",
                                 claim_code='AAABBBCCC', secret="12345", gcm_id="123")

        # and claim it
        self.fetch_page(reverse('channels.channel_claim_android'))
        self.keys('#id_claim_code', 'AAABBBCCC')
        self.keys('#id_phone_number', '0785551212')
        self.submit('.claim-form')

        # get our freshly claimed channel
        channel = Channel.objects.get(pk=channel.pk)

        # now go to the contacts page
        self.click('#menu-right .icon-contact')
        self.click('#id_import_contacts')

        # upload some contacts
        directory = os.path.dirname(os.path.realpath(__file__))
        self.keys('#csv_file', '%s/../media/test_imports/sample_contacts.xls' % directory)
        self.submit('.smartmin-form')

        # make sure they are there
        self.click('#menu-right .icon-contact')
        self.assertInElements('.value-phone', '+250788382382')
        self.assertInElements('.value-text', 'Eric Newcomer')
        self.assertInElements('.value-text', 'Sample Contacts')
Esempio n. 44
0
    def form_valid(self, form):
        org = self.request.user.get_org()
        data = form.cleaned_data
        config = {Channel.CONFIG_USERNAME: data["username"], Channel.CONFIG_PASSWORD: data["password"]}

        self.object = Channel.create(
            org=org,
            user=self.request.user,
            country=data["country"],
            channel_type="MT",
            name=data["service_id"],
            address=data["service_id"],
            config=config,
            schemes=[TEL_SCHEME],
        )

        return super().form_valid(form)
Esempio n. 45
0
    def form_valid(self, form):
        org = self.request.user.get_org()

        if not org:  # pragma: no cover
            raise Exception(_("No org for this user, cannot claim"))

        data = form.cleaned_data
        self.object = Channel.add_config_external_channel(
            org,
            self.request.user,
            "SO",
            data["number"],
            "SQ",
            dict(send_url=data["url"], username=data["username"], password=data["password"]),
        )

        return super(AuthenticatedExternalClaimView, self).form_valid(form)
Esempio n. 46
0
    def unregister_active_event(self):
        """
        Helper function for unregistering active events on a throttled channel
        """
        r = get_redis_connection()

        channel_key = Channel.redis_active_events_key(self.channel_id)
        # are we on a throttled channel?
        current_tracked_events = r.get(channel_key)

        if current_tracked_events:

            value = int(current_tracked_events)
            if value <= 0:  # pragma: no cover
                raise ValueError("When this happens I'll quit my job and start producing moonshine/poitin/brlja !")

            r.decr(channel_key)
Esempio n. 47
0
    def setUp(self):
        super(APITest, self).setUp()

        self.joe = self.create_contact("Joe Blow", "0788123123")
        self.frank = self.create_contact("Frank", twitter="franky")
        self.test_contact = Contact.get_test_contact(self.user)

        self.twitter = Channel.create(self.org, self.user, None, 'TT', name="Twitter Channel",
                                      address="billy_bob", role="SR", scheme='twitter')

        self.create_secondary_org()
        self.hans = self.create_contact("Hans Gruber", "+4921551511", org=self.org2)

        self.maxDiff = None

        # this is needed to prevent REST framework from rolling back transaction created around each unit test
        connection.settings_dict['ATOMIC_REQUESTS'] = False
Esempio n. 48
0
    def form_valid(self, form):
        org = self.request.user.get_org()

        if not org:  # pragma: no cover
            raise Exception(_("No org for this user, cannot claim"))

        data = form.cleaned_data
        self.object = Channel.add_config_external_channel(
            org,
            self.request.user,
            "PH",
            data["number"],
            "GL",
            dict(app_id=data["app_id"], app_secret=data["app_secret"], passphrase=data["passphrase"]),
            role=Channel.ROLE_SEND + Channel.ROLE_RECEIVE,
        )

        return super(AuthenticatedExternalClaimView, self).form_valid(form)
Esempio n. 49
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)
Esempio n. 50
0
    def form_valid(self, form):
        org = self.request.user.get_org()
        data = form.cleaned_data
        config = {Channel.CONFIG_CALLBACK_DOMAIN: org.get_brand_domain()}
        if data["secret"]:
            config[Channel.CONFIG_SECRET] = data["secret"]

        self.object = Channel.add_authenticated_external_channel(
            org,
            self.request.user,
            self.get_submitted_country(data),
            data["number"],
            data["username"],
            data["password"],
            "JN",
            data.get("url"),
            role=Channel.DEFAULT_ROLE,
            extra_config=config,
        )

        return super(AuthenticatedExternalClaimView, self).form_valid(form)
Esempio n. 51
0
    def form_valid(self, form):
        org = self.request.user.get_org()
        data = form.cleaned_data

        config = None
        if data["secret"]:
            config = {Channel.CONFIG_SECRET: data["secret"]}

        self.object = Channel.add_authenticated_external_channel(
            org,
            self.request.user,
            self.get_submitted_country(data),
            data["number"],
            data["username"],
            data["password"],
            "JNU",
            data.get("url"),
            extra_config=config,
            role=Channel.ROLE_USSD,
        )

        return super(AuthenticatedExternalClaimView, self).form_valid(form)
Esempio n. 52
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)
Esempio n. 53
0
    def test_new_conversation_trigger(self):
        self.login(self.admin)
        flow = self.create_flow()
        flow2 = self.create_flow()

        # see if we list new conversation triggers on the trigger page
        create_trigger_url = reverse('triggers.trigger_create', args=[])
        response = self.client.get(create_trigger_url)
        self.assertNotContains(response, "conversation is started")

        # create a facebook channel
        fb_channel = Channel.add_facebook_channel(self.org, self.user, 'Temba', 1001, 'fb_token')

        # should now be able to create one
        response = self.client.get(create_trigger_url)
        self.assertContains(response, "conversation is started")

        # go create it
        with patch('requests.post') as mock_post:
            mock_post.return_value = MockResponse(200, '{"message": "Success"}')

            response = self.client.post(reverse('triggers.trigger_new_conversation', args=[]),
                                        data=dict(channel=fb_channel.id, flow=flow.id))
            self.assertEqual(response.status_code, 200)
            self.assertEqual(mock_post.call_count, 1)

        # check that it is right
        trigger = Trigger.objects.get(trigger_type=Trigger.TYPE_NEW_CONVERSATION, is_active=True, is_archived=False)
        self.assertEqual(trigger.channel, fb_channel)
        self.assertEqual(trigger.flow, flow)

        # try to create another one, fails as we already have a trigger for that channel
        response = self.client.post(reverse('triggers.trigger_new_conversation', args=[]), data=dict(channel=fb_channel.id, flow=flow2.id))
        self.assertEqual(response.status_code, 200)
        self.assertFormError(response, 'form', 'channel', 'Trigger with this Channel already exists.')

        # ok, trigger a facebook event
        data = json.loads("""{
        "object": "page",
          "entry": [
            {
              "id": "620308107999975",
              "time": 1467841778778,
              "messaging": [
                {
                  "sender":{
                    "id":"1001"
                  },
                  "recipient":{
                    "id":"%s"
                  },
                  "timestamp":1458692752478,
                  "postback":{
                    "payload":"get_started"
                  }
                }
              ]
            }
          ]
        }
        """ % fb_channel.address)

        with patch('requests.get') as mock_get:
            mock_get.return_value = MockResponse(200, '{"first_name": "Ben","last_name": "Haggerty"}')

            callback_url = reverse('handlers.facebook_handler', args=[fb_channel.uuid])
            response = self.client.post(callback_url, json.dumps(data), content_type="application/json")
            self.assertEqual(response.status_code, 200)

            # should have a new flow run for Ben
            contact = Contact.from_urn(self.org, 'facebook:1001')
            self.assertTrue(contact.name, "Ben Haggerty")

            run = FlowRun.objects.get(contact=contact)
            self.assertEqual(run.flow, flow)

        # archive our trigger, should unregister our callback
        with patch('requests.post') as mock_post:
            mock_post.return_value = MockResponse(200, '{"message": "Success"}')

            Trigger.apply_action_archive(self.admin, Trigger.objects.filter(pk=trigger.pk))
            self.assertEqual(response.status_code, 200)
            self.assertEqual(mock_post.call_count, 1)

            trigger.refresh_from_db()
            self.assertTrue(trigger.is_archived)
Esempio n. 54
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,
            )
Esempio n. 55
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
Esempio n. 56
0
    def claim_number(self, user, phone_number, country, role):

        auth_id = self.request.session.get(Channel.CONFIG_PLIVO_AUTH_ID, None)
        auth_token = self.request.session.get(Channel.CONFIG_PLIVO_AUTH_TOKEN, None)

        org = user.get_org()

        plivo_uuid = generate_uuid()
        callback_domain = org.get_brand_domain()
        app_name = "%s/%s" % (callback_domain.lower(), plivo_uuid)

        message_url = "https://" + callback_domain + "%s" % reverse("courier.pl", args=[plivo_uuid, "receive"])
        answer_url = "https://" + settings.TEMBA_HOST+settings.MEDIA_URL+ "plivo_voice_unavailable.xml"

        headers = http_headers(extra={"Content-Type": "application/json"})
        create_app_url = "https://api.plivo.com/v1/Account/%s/Application/" % auth_id

        response = requests.post(
            create_app_url,
            json=dict(app_name=app_name, answer_url=answer_url, message_url=message_url),
            headers=headers,
            auth=(auth_id, auth_token),
        )

        if response.status_code in [201, 200, 202]:
            plivo_app_id = response.json()["app_id"]
        else:  # pragma: no cover
            plivo_app_id = None

        plivo_config = {
            Channel.CONFIG_PLIVO_AUTH_ID: auth_id,
            Channel.CONFIG_PLIVO_AUTH_TOKEN: auth_token,
            Channel.CONFIG_PLIVO_APP_ID: plivo_app_id,
            Channel.CONFIG_CALLBACK_DOMAIN: org.get_brand_domain(),
        }

        plivo_number = phone_number.strip("+ ").replace(" ", "")
        response = requests.get(
            "https://api.plivo.com/v1/Account/%s/Number/%s/" % (auth_id, plivo_number),
            headers=headers,
            auth=(auth_id, auth_token),
        )

        if response.status_code != 200:
            response = requests.post(
                "https://api.plivo.com/v1/Account/%s/PhoneNumber/%s/" % (auth_id, plivo_number),
                headers=headers,
                auth=(auth_id, auth_token),
            )

            if response.status_code != 201:  # pragma: no cover
                raise Exception(
                    _("There was a problem claiming that number, please check the balance on your account.")
                )

            response = requests.get(
                "https://api.plivo.com/v1/Account/%s/Number/%s/" % (auth_id, plivo_number),
                headers=headers,
                auth=(auth_id, auth_token),
            )

        if response.status_code == 200:
            response = requests.post(
                "https://api.plivo.com/v1/Account/%s/Number/%s/" % (auth_id, plivo_number),
                json=dict(app_id=plivo_app_id),
                headers=headers,
                auth=(auth_id, auth_token),
            )

            if response.status_code != 202:  # pragma: no cover
                raise Exception(_("There was a problem updating that number, please try again."))

        phone_number = "+" + plivo_number
        phone = phonenumbers.format_number(
            phonenumbers.parse(phone_number, None), phonenumbers.PhoneNumberFormat.NATIONAL
        )

        channel = Channel.create(
            org, user, country, "PL", name=phone, address=phone_number, config=plivo_config, uuid=plivo_uuid
        )

        analytics.track(user.username, "temba.channel_claim_plivo", dict(number=phone_number))

        return channel
Esempio n. 57
0
    def claim_number(self, user, phone_number, country, role):
        org = user.get_org()

        client = org.get_nexmo_client()
        org_config = org.config
        app_id = org_config.get(NEXMO_APP_ID)

        nexmo_phones = client.get_numbers(phone_number)
        is_shortcode = False

        # try it with just the national code (for short codes)
        if not nexmo_phones:
            parsed = phonenumbers.parse(phone_number, None)
            shortcode = str(parsed.national_number)

            nexmo_phones = client.get_numbers(shortcode)

            if nexmo_phones:
                is_shortcode = True
                phone_number = shortcode

        # buy the number if we have to
        if not nexmo_phones:
            try:
                client.buy_nexmo_number(country, phone_number)
            except Exception as e:
                raise Exception(
                    _(
                        "There was a problem claiming that number, "
                        "please check the balance on your account. " + "Note that you can only claim numbers after "
                        "adding credit to your Nexmo account."
                    )
                    + "\n"
                    + str(e)
                )

        channel_uuid = generate_uuid()
        callback_domain = org.get_brand_domain()
        new_receive_url = "https://" + callback_domain + reverse("courier.nx", args=[channel_uuid, "receive"])

        nexmo_phones = client.get_numbers(phone_number)

        features = [elt.upper() for elt in nexmo_phones[0]["features"]]
        role = ""
        if "SMS" in features:
            role += Channel.ROLE_SEND + Channel.ROLE_RECEIVE

        if "VOICE" in features:
            role += Channel.ROLE_ANSWER + Channel.ROLE_CALL

        # update the delivery URLs for it
        try:
            client.update_nexmo_number(country, phone_number, new_receive_url, app_id)

        except Exception as e:  # pragma: no cover
            # shortcodes don't seem to claim right on nexmo, move forward anyways
            if not is_shortcode:
                raise Exception(
                    _("There was a problem claiming that number, please check the balance on your account.")
                    + "\n"
                    + str(e)
                )

        if is_shortcode:
            phone = phone_number
            nexmo_phone_number = phone_number
        else:
            parsed = phonenumbers.parse(phone_number, None)
            phone = phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.INTERNATIONAL)

            # nexmo ships numbers around as E164 without the leading +
            nexmo_phone_number = phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164).strip("+")

        config = {
            Channel.CONFIG_NEXMO_APP_ID: app_id,
            Channel.CONFIG_NEXMO_APP_PRIVATE_KEY: org_config[NEXMO_APP_PRIVATE_KEY],
            Channel.CONFIG_NEXMO_API_KEY: org_config[NEXMO_KEY],
            Channel.CONFIG_NEXMO_API_SECRET: org_config[NEXMO_SECRET],
            Channel.CONFIG_CALLBACK_DOMAIN: callback_domain,
        }

        channel = Channel.create(
            org,
            user,
            country,
            "NX",
            name=phone,
            address=phone_number,
            role=role,
            config=config,
            bod=nexmo_phone_number,
            uuid=channel_uuid,
            tps=1,
        )

        analytics.track(user.username, "temba.channel_claim_nexmo", dict(number=phone_number))

        return channel
Esempio n. 58
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
Esempio n. 59
0
    def test_ivr_flow(self):
        from temba.orgs.models import ACCOUNT_TOKEN, ACCOUNT_SID

        # should be able to create an ivr flow
        self.assertTrue(self.org.supports_ivr())
        self.assertTrue(self.admin.groups.filter(name="Beta"))
        self.assertContains(self.client.get(reverse('flows.flow_create')), 'Phone Call')

        # no twilio config yet
        self.assertFalse(self.org.is_connected_to_twilio())
        self.assertIsNone(self.org.get_twilio_client())

        # connect it and check our client is configured
        self.org.connect_twilio("TEST_SID", "TEST_TOKEN", self.admin)
        self.org.save()
        self.assertTrue(self.org.is_connected_to_twilio())
        self.assertIsNotNone(self.org.get_twilio_client())

        # no twiml api config yet
        self.assertIsNone(self.channel.get_twiml_client())

        # twiml api config
        config = {Channel.CONFIG_SEND_URL: 'https://api.twilio.com',
                  ACCOUNT_SID: 'TEST_SID',
                  ACCOUNT_TOKEN: 'TEST_TOKEN'}
        channel = Channel.add_twiml_api_channel(self.org, self.org.get_user(), 'BR', '558299990000', config, 'AC')
        self.assertEqual(channel.org, self.org)
        self.assertEqual(channel.address, '+558299990000')

        # import an ivr flow
        self.import_file('call_me_maybe')

        # make sure our flow is there as expected
        flow = Flow.objects.filter(name='Call me maybe').first()
        self.assertEquals('callme', flow.triggers.filter(trigger_type='K').first().keyword)

        user_settings = self.admin.get_settings()
        user_settings.tel = '+18005551212'
        user_settings.save()

        # start our flow as a test contact
        test_contact = Contact.get_test_contact(self.admin)
        Contact.set_simulation(True)
        flow.start([], [test_contact])
        call = IVRCall.objects.filter(direction=OUTGOING).first()

        # should be using the usersettings number in test mode
        self.assertEquals('Placing test call to +1 800-555-1212', ActionLog.objects.all().first().text)

        # our twilio callback on pickup
        post_data = dict(CallSid='CallSid', CallStatus='in-progress', CallDuration=20)
        response = self.client.post(reverse('ivr.ivrcall_handle', args=[call.pk]), post_data)

        # simulate a button press and that our message is handled
        response = self.client.post(reverse('ivr.ivrcall_handle', args=[call.pk]), dict(Digits=4))
        msg = Msg.objects.filter(contact=test_contact, text="4", direction='I').first()
        self.assertIsNotNone(msg)
        self.assertEqual('H', msg.status)

        # explicitly hanging up on a test call should remove it
        call.update_status('in-progress', 0)
        call.save()
        IVRCall.hangup_test_call(flow)
        self.assertTrue(IVRCall.objects.filter(pk=call.pk).first())

        ActionLog.objects.all().delete()
        IVRCall.objects.all().delete()
        Msg.objects.all().delete()

        # now pretend we are a normal caller
        eric = self.create_contact('Eric Newcomer', number='+13603621737')
        Contact.set_simulation(False)
        flow.start([], [eric], restart_participants=True)

        # we should have an outbound ivr call now
        call = IVRCall.objects.filter(direction=OUTGOING).first()

        self.assertEquals(0, call.get_duration())
        self.assertIsNotNone(call)
        self.assertEquals('CallSid', call.external_id)

        # after a call is picked up, twilio will call back to our server
        post_data = dict(CallSid='CallSid', CallStatus='in-progress', CallDuration=20)
        response = self.client.post(reverse('ivr.ivrcall_handle', args=[call.pk]), post_data)

        self.assertContains(response, '<Say>Would you like me to call you? Press one for yes, two for no, or three for maybe.</Say>')
        self.assertEquals(1, Msg.objects.filter(msg_type=IVR).count())
        self.assertEquals(1, self.org.get_credits_used())

        # make sure a message from the person on the call goes to the
        # inbox since our flow doesn't handle text messages
        msg = self.create_msg(direction='I', contact=eric, text="message during phone call")
        self.assertFalse(Flow.find_and_handle(msg))

        # updated our status and duration accordingly
        call = IVRCall.objects.get(pk=call.pk)
        self.assertEquals(20, call.duration)
        self.assertEquals(IN_PROGRESS, call.status)

        # don't press any numbers, but # instead
        response = self.client.post(reverse('ivr.ivrcall_handle', args=[call.pk]) + "?empty=1", dict())
        self.assertContains(response, '<Say>Press one, two, or three. Thanks.</Say>')
        self.assertEquals(4, self.org.get_credits_used())

        # press the number 4 (unexpected)
        response = self.client.post(reverse('ivr.ivrcall_handle', args=[call.pk]), dict(Digits=4))

        # our inbound message should be handled
        msg = Msg.objects.filter(text='4', msg_type=IVR).order_by('-created_on').first()
        self.assertEqual('H', msg.status)

        self.assertContains(response, '<Say>Press one, two, or three. Thanks.</Say>')
        self.assertEquals(6, self.org.get_credits_used())

        # two more messages, one inbound and it's response
        self.assertEquals(5, Msg.objects.filter(msg_type=IVR).count())

        # now let's have them press the number 3 (for maybe)
        response = self.client.post(reverse('ivr.ivrcall_handle', args=[call.pk]), dict(Digits=3))
        self.assertContains(response, '<Say>This might be crazy.</Say>')
        messages = Msg.objects.filter(msg_type=IVR).order_by('pk')
        self.assertEquals(7, messages.count())
        self.assertEquals(8, self.org.get_credits_used())

        for msg in messages:
            self.assertEquals(1, msg.steps.all().count(), msg="Message '%s' not attached to step" % msg.text)

        # twilio would then disconnect the user and notify us of a completed call
        self.client.post(reverse('ivr.ivrcall_handle', args=[call.pk]), dict(CallStatus='completed'))
        call = IVRCall.objects.get(pk=call.pk)
        self.assertEquals(COMPLETED, call.status)
        self.assertFalse(FlowRun.objects.filter(call=call).first().is_active)

        # simulation gets flipped off by middleware, and this unhandled message doesn't flip it back on
        self.assertFalse(Contact.get_simulation())

        # also shouldn't have any ActionLogs for non-test users
        self.assertEquals(0, ActionLog.objects.all().count())
        self.assertEquals(1, flow.get_completed_runs())

        # should still have no active runs
        self.assertEquals(0, FlowRun.objects.filter(is_active=True).count())

        # and we've exited the flow
        step = FlowStep.objects.all().order_by('-pk').first()
        self.assertTrue(step.left_on)

        # test other our call status mappings with twilio
        def test_status_update(call_to_update, twilio_status, temba_status):
            call_to_update.update_status(twilio_status, 0)
            call_to_update.save()
            self.assertEquals(temba_status, IVRCall.objects.get(pk=call_to_update.pk).status)

        test_status_update(call, 'queued', QUEUED)
        test_status_update(call, 'ringing', RINGING)
        test_status_update(call, 'canceled', CANCELED)
        test_status_update(call, 'busy', BUSY)
        test_status_update(call, 'failed', FAILED)
        test_status_update(call, 'no-answer', NO_ANSWER)

        FlowStep.objects.all().delete()
        IVRCall.objects.all().delete()

        # try sending callme trigger
        from temba.msgs.models import INCOMING
        msg = self.create_msg(direction=INCOMING, contact=eric, text="callme")

        # make sure if we are started with a message we still create a normal voice run
        flow.start([], [eric], restart_participants=True, start_msg=msg)

        # we should have an outbound ivr call now, and no steps yet
        call = IVRCall.objects.filter(direction=OUTGOING).first()
        self.assertIsNotNone(call)
        self.assertEquals(0, FlowStep.objects.all().count())

        # after a call is picked up, twilio will call back to our server
        post_data = dict(CallSid='CallSid', CallStatus='in-progress', CallDuration=20)
        self.client.post(reverse('ivr.ivrcall_handle', args=[call.pk]), post_data)

        # should have two flow steps (the outgoing messages, and the step to handle the response)
        steps = FlowStep.objects.all().order_by('pk')

        # the first step has exactly one message which is an outgoing IVR message
        self.assertEquals(1, steps.first().messages.all().count())
        self.assertEquals(1, steps.first().messages.filter(direction=OUTGOING, msg_type=IVR).count())

        # the next step shouldn't have any messages yet since they haven't pressed anything
        self.assertEquals(0, steps[1].messages.all().count())

        # try updating our status to completed for a test contact
        Contact.set_simulation(True)
        flow.start([], [test_contact])
        call = IVRCall.objects.filter(direction=OUTGOING).order_by('-pk').first()
        call.update_status('completed', 30)
        call.save()
        call.refresh_from_db()

        self.assertEqual(ActionLog.objects.all().order_by('-pk').first().text, 'Call ended.')
        self.assertEqual(call.duration, 30)

        # now look at implied duration
        call.update_status('in-progress', None)
        call.save()
        call.refresh_from_db()
        self.assertIsNotNone(call.get_duration())