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)
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)
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)
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')
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)
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)
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
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)
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
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
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)
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
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
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()
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)
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)
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'])
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)
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, )
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()
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"}], }, } ], )
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, )
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')
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)
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)
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)
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)
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
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)
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)
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)
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")
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)
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)
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])
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", )
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()
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)
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 )
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')
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)
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)
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)
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)
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)
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)
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)
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)
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)
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, )
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
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
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
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
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())