def clean(self): # first check that our phone number looks sane number, valid = URN.normalize_number(self.cleaned_data['number'], self.cleaned_data['country']) if not valid: raise forms.ValidationError( _("Please enter a valid phone number")) self.cleaned_data['number'] = number try: resp = requests.post(self.cleaned_data['base_url'] + '/api/check_health.php', json=dict(payload=['gateway_status']), auth=(self.cleaned_data['username'], self.cleaned_data['password'])) if resp.status_code != 200: raise Exception("Received non-200 response: %d", resp.status_code) except Exception: raise forms.ValidationError( _("Unable to check WhatsApp enterprise account, please check username and password" )) return self.cleaned_data
def clean(self): # first check that our phone number looks sane number, valid = URN.normalize_number(self.cleaned_data["number"], self.cleaned_data["country"]) if not valid: raise forms.ValidationError( _("Please enter a valid phone number")) self.cleaned_data["number"] = number try: resp = requests.post( self.cleaned_data["base_url"] + "/v1/users/login", auth=(self.cleaned_data["username"], self.cleaned_data["password"]), ) if resp.status_code != 200: raise Exception("Received non-200 response: %d", resp.status_code) self.cleaned_data["auth_token"] = resp.json( )["users"][0]["token"] except Exception: raise forms.ValidationError( _("Unable to check WhatsApp enterprise account, please check username and password" )) return self.cleaned_data
def clean(self): # first check that our phone number looks sane number, valid = URN.normalize_number(self.cleaned_data['number'], self.cleaned_data['country']) if not valid: raise forms.ValidationError( _("Please enter a valid phone number")) self.cleaned_data['number'] = number try: resp = requests.post(self.cleaned_data['base_url'] + '/v1/users/login', auth=(self.cleaned_data['username'], self.cleaned_data['password'])) if resp.status_code != 200: raise Exception("Received non-200 response: %d", resp.status_code) self.cleaned_data['auth_token'] = resp.json( )['users'][0]['token'] except Exception: raise forms.ValidationError( _("Unable to check WhatsApp enterprise account, please check username and password" )) return self.cleaned_data
def clean_number(self): # check that our phone number looks sane country = self.data["country"] number = URN.normalize_number(self.data["number"], country) if not URN.validate(URN.from_parts(URN.TEL_SCHEME, number), country): raise forms.ValidationError( _("Please enter a valid phone number")) return number
def clean(self): # first check that our phone number looks sane country = self.cleaned_data["country"] normalized = URN.normalize_number(self.cleaned_data["number"], country) if not URN.validate(URN.from_parts(URN.TEL_SCHEME, normalized), country): raise forms.ValidationError(_("Please enter a valid phone number")) self.cleaned_data["number"] = normalized return self.cleaned_data
def clean(self): # first check that our phone number looks sane number, valid = URN.normalize_number(self.cleaned_data["number"], self.cleaned_data["country"]) if not valid: raise forms.ValidationError( _("Please enter a valid phone number")) self.cleaned_data["number"] = number return self.cleaned_data
def clean(self): # first check that our phone number looks sane country = self.cleaned_data["country"] normalized = URN.normalize_number(self.cleaned_data["number"], country) if not URN.validate(URN.from_parts(URN.TEL_SCHEME, normalized), country): raise forms.ValidationError( _("Please enter a valid phone number")) self.cleaned_data["number"] = normalized try: resp = requests.post( self.cleaned_data["base_url"] + "/v1/users/login", auth=(self.cleaned_data["username"], self.cleaned_data["password"]), ) if resp.status_code != 200: raise Exception("Received non-200 response: %d", resp.status_code) self.cleaned_data["auth_token"] = resp.json( )["users"][0]["token"] except Exception: raise forms.ValidationError( _("Unable to check WhatsApp enterprise account, please check username and password" )) # check we can access their facebook templates from .type import TEMPLATE_LIST_URL if self.cleaned_data[ "facebook_template_list_domain"] != "graph.facebook.com": response = requests.get( TEMPLATE_LIST_URL % (self.cleaned_data["facebook_template_list_domain"], self.cleaned_data["facebook_business_id"]), params=dict(access_token=self. cleaned_data["facebook_access_token"]), ) if response.status_code != 200: raise forms.ValidationError( _("Unable to access Facebook templates, please check user id and access token and make sure " + "the whatsapp_business_management permission is enabled" )) return self.cleaned_data
def clean(self): # first check that our phone number looks sane number, valid = URN.normalize_number(self.cleaned_data["number"], self.cleaned_data["country"]) if not valid: raise forms.ValidationError(_("Please enter a valid phone number")) self.cleaned_data["number"] = number try: resp = requests.post( self.cleaned_data["base_url"] + "/v1/users/login", auth=(self.cleaned_data["username"], self.cleaned_data["password"]), ) if resp.status_code != 200: raise Exception("Received non-200 response: %d", resp.status_code) self.cleaned_data["auth_token"] = resp.json()["users"][0]["token"] except Exception: raise forms.ValidationError( _("Unable to check WhatsApp enterprise account, please check username and password") ) return self.cleaned_data
def post(self, request, *args, **kwargs): from twilio.request_validator import RequestValidator from temba.flows.models import FlowSession signature = request.META.get("HTTP_X_TWILIO_SIGNATURE", "") url = "https://" + request.get_host() + "%s" % request.get_full_path() channel_uuid = kwargs.get("uuid") call_sid = self.get_param("CallSid") direction = self.get_param("Direction") status = self.get_param("CallStatus") to_number = self.get_param("To") to_country = self.get_param("ToCountry") from_number = self.get_param("From") # Twilio sometimes sends un-normalized numbers if to_number and not to_number.startswith("+") and to_country: # pragma: no cover to_number, valid = URN.normalize_number(to_number, to_country) # see if it's a twilio call being initiated if to_number and call_sid and direction == "inbound" and status == "ringing": # find a channel that knows how to answer twilio calls channel = self.get_ringing_channel(uuid=channel_uuid) if not channel: response = VoiceResponse() response.say("Sorry, there is no channel configured to take this call. Goodbye.") response.hangup() return HttpResponse(str(response)) org = channel.org if self.get_channel_type() == "T" and not org.is_connected_to_twilio(): return HttpResponse("No Twilio account is connected", status=400) client = self.get_client(channel=channel) validator = RequestValidator(client.auth[1]) signature = request.META.get("HTTP_X_TWILIO_SIGNATURE", "") url = "https://%s%s" % (request.get_host(), request.get_full_path()) if validator.validate(url, request.POST, signature): from temba.ivr.models import IVRCall # find a contact for the one initiating us urn = URN.from_tel(from_number) contact, urn_obj = Contact.get_or_create(channel.org, urn, channel) flow = Trigger.find_flow_for_inbound_call(contact) if flow: call = IVRCall.create_incoming(channel, contact, urn_obj, channel.created_by, call_sid) session = FlowSession.create(contact, connection=call) call.update_status( request.POST.get("CallStatus", None), request.POST.get("CallDuration", None), "T" ) call.save() FlowRun.create(flow, contact, session=session, connection=call) response = Flow.handle_call(call) return HttpResponse(str(response)) else: # we don't have an inbound trigger to deal with this call. response = channel.generate_ivr_response() # say nothing and hangup, this is a little rude, but if we reject the call, then # they'll get a non-working number error. We send 'busy' when our server is down # so we don't want to use that here either. response.say("") response.hangup() # if they have a missed call trigger, fire that off Trigger.catch_triggers(contact, Trigger.TYPE_MISSED_CALL, channel) # either way, we need to hangup now return HttpResponse(str(response)) # check for call progress events, these include post-call hangup notifications if request.POST.get("CallbackSource", None) == "call-progress-events": if call_sid: from temba.ivr.models import IVRCall call = IVRCall.objects.filter(external_id=call_sid).first() if call: call.update_status( request.POST.get("CallStatus", None), request.POST.get("CallDuration", None), "TW" ) call.save() return HttpResponse("Call status updated") return HttpResponse("No call found") return HttpResponse("Not Handled, unknown action", status=400) # pragma: no cover
def test_claim(self): # remove our explicit country so it needs to be derived from channels self.org.country = None self.org.save() Channel.objects.all().delete() reg_data = dict(cmds=[ dict(cmd="fcm", fcm_id="FCM111", uuid="uuid"), dict(cmd="status", cc="RW", dev="Nexus") ]) # must be a post response = self.client.get(reverse("register"), content_type="application/json") self.assertEqual(500, response.status_code) # try a legit register response = self.client.post(reverse("register"), json.dumps(reg_data), content_type="application/json") self.assertEqual(200, response.status_code) android1 = Channel.objects.get() self.assertIsNone(android1.org) self.assertIsNone(android1.address) self.assertIsNone(android1.alert_email) self.assertEqual(android1.country, "RW") self.assertEqual(android1.device, "Nexus") self.assertEqual(android1.config["FCM_ID"], "FCM111") self.assertEqual(android1.uuid, "uuid") self.assertTrue(android1.secret) self.assertTrue(android1.claim_code) self.assertEqual(android1.created_by, get_anonymous_user()) # check channel JSON in response response_json = response.json() self.assertEqual( response_json, dict(cmds=[ dict( cmd="reg", relayer_claim_code=android1.claim_code, relayer_secret=android1.secret, relayer_id=android1.id, ) ]), ) # try registering again with same details response = self.client.post(reverse("register"), json.dumps(reg_data), content_type="application/json") self.assertEqual(response.status_code, 200) android1 = Channel.objects.get() response_json = response.json() self.assertEqual( response_json, dict(cmds=[ dict( cmd="reg", relayer_claim_code=android1.claim_code, relayer_secret=android1.secret, relayer_id=android1.id, ) ]), ) # view claim page self.login(self.admin) response = self.client.get(reverse("channels.types.android.claim")) self.assertContains(response, "https://app.rapidpro.io/android/") # try to claim as non-admin self.login(self.user) response = self.client.post( reverse("channels.types.android.claim"), dict(claim_code=android1.claim_code, phone_number="0788123123")) self.assertLoginRedirect(response) # try to claim with an invalid phone number self.login(self.admin) response = self.client.post( reverse("channels.types.android.claim"), dict(claim_code=android1.claim_code, phone_number="078123")) self.assertEqual(response.status_code, 200) self.assertFormError(response, "form", "phone_number", "Invalid phone number, try again.") # claim our channel response = self.client.post( reverse("channels.types.android.claim"), dict(claim_code=android1.claim_code, phone_number="0788123123")) # redirect to welcome page self.assertIn("success", response.get("Location", None)) self.assertRedirect(response, reverse("public.public_welcome")) # channel is updated with org details and claim code is now blank android1.refresh_from_db() secret = android1.secret self.assertEqual(android1.org, self.org) self.assertEqual(android1.address, "+250788123123") # normalized self.assertEqual(android1.alert_email, self.admin.email) # the logged-in user self.assertEqual(android1.config["FCM_ID"], "FCM111") self.assertEqual(android1.uuid, "uuid") self.assertFalse(android1.claim_code) # try having a device register again response = self.client.post(reverse("register"), json.dumps(reg_data), content_type="application/json") self.assertEqual(response.status_code, 200) # should return same channel but with a new claim code and secret android1.refresh_from_db() self.assertEqual(android1.org, self.org) self.assertEqual(android1.address, "+250788123123") self.assertEqual(android1.alert_email, self.admin.email) self.assertEqual(android1.config["FCM_ID"], "FCM111") self.assertEqual(android1.uuid, "uuid") self.assertEqual(android1.is_active, True) self.assertTrue(android1.claim_code) self.assertNotEqual(android1.secret, secret) # should be able to claim again response = self.client.post( reverse("channels.types.android.claim"), dict(claim_code=android1.claim_code, phone_number="0788123123")) self.assertRedirect(response, reverse("public.public_welcome")) # try having a device register yet again with new FCM ID reg_data["cmds"][0]["fcm_id"] = "FCM222" response = self.client.post(reverse("register"), json.dumps(reg_data), content_type="application/json") self.assertEqual(response.status_code, 200) # should return same channel but with FCM updated android1.refresh_from_db() self.assertEqual(android1.org, self.org) self.assertEqual(android1.address, "+250788123123") self.assertEqual(android1.alert_email, self.admin.email) self.assertEqual(android1.config["FCM_ID"], "FCM222") self.assertEqual(android1.uuid, "uuid") self.assertEqual(android1.is_active, True) # we can claim again with new phone number response = self.client.post( reverse("channels.types.android.claim"), dict(claim_code=android1.claim_code, phone_number="+250788123124")) self.assertRedirect(response, reverse("public.public_welcome")) android1.refresh_from_db() self.assertEqual(android1.org, self.org) self.assertEqual(android1.address, "+250788123124") self.assertEqual(android1.alert_email, self.admin.email) self.assertEqual(android1.config["FCM_ID"], "FCM222") self.assertEqual(android1.uuid, "uuid") self.assertEqual(android1.is_active, True) # release and then register with same details and claim again old_uuid = android1.uuid android1.release() response = self.client.post(reverse("register"), json.dumps(reg_data), content_type="application/json") claim_code = response.json()["cmds"][0]["relayer_claim_code"] self.assertEqual(response.status_code, 200) response = self.client.post( reverse("channels.types.android.claim"), dict(claim_code=claim_code, phone_number="+250788123124")) self.assertRedirect(response, reverse("public.public_welcome")) android1.refresh_from_db() self.assertNotEqual(android1.uuid, old_uuid) # inactive channel now has new UUID # and we have a new Android channel with our UUID android2 = Channel.objects.get(is_active=True) self.assertNotEqual(android2, android1) self.assertEqual(android2.uuid, "uuid") # try to claim a bogus channel response = self.client.post(reverse("channels.types.android.claim"), dict(claim_code="Your Mom")) self.assertEqual(response.status_code, 200) self.assertFormError( response, "form", "claim_code", "Invalid claim code, please check and try again.") # check our primary tel channel is the same as our outgoing default_sender = self.org.get_send_channel(TEL_SCHEME) self.assertEqual(default_sender, android2) self.assertEqual(default_sender, self.org.get_receive_channel(TEL_SCHEME)) self.assertFalse(default_sender.is_delegate_sender()) response = self.client.get( reverse("channels.channel_bulk_sender_options")) self.assertEqual(response.status_code, 200) response = self.client.post( reverse("channels.channel_create_bulk_sender") + "?connection=NX", dict(connection="NX")) self.assertFormError(response, "form", "channel", "Can't add sender for that number") # try to claim a bulk Nexmo sender (without adding Nexmo account to org) claim_nexmo_url = reverse("channels.channel_create_bulk_sender" ) + "?connection=NX&channel=%d" % android2.pk response = self.client.post(claim_nexmo_url, dict(connection="NX", channel=android2.pk)) self.assertFormError(response, "form", "connection", "A connection to a Nexmo account is required") # send channel is still our Android device self.assertEqual(self.org.get_send_channel(TEL_SCHEME), android2) self.assertFalse(self.org.is_connected_to_nexmo()) # now connect to nexmo self.org.connect_nexmo("123", "456", self.admin) self.assertTrue(self.org.is_connected_to_nexmo()) # now adding Nexmo bulk sender should work response = self.client.post(claim_nexmo_url, dict(connection="NX", channel=android2.pk)) self.assertRedirect(response, reverse("orgs.org_home")) # new Nexmo channel created for delegated sending nexmo = self.org.get_send_channel(TEL_SCHEME) self.assertEqual(nexmo.channel_type, "NX") self.assertEqual(nexmo.parent, android2) self.assertTrue(nexmo.is_delegate_sender()) self.assertEqual(nexmo.tps, 1) channel_config = nexmo.config self.assertEqual(channel_config[Channel.CONFIG_NEXMO_API_KEY], "123") self.assertEqual(channel_config[Channel.CONFIG_NEXMO_API_SECRET], "456") # reading our nexmo channel should now offer a disconnect option nexmo = self.org.channels.filter(channel_type="NX").first() response = self.client.get( reverse("channels.channel_read", args=[nexmo.uuid])) self.assertContains(response, "Disable Bulk Sending") # receiving still job of our Android device self.assertEqual(self.org.get_receive_channel(TEL_SCHEME), android2) # re-register device with country as US reg_data = dict(cmds=[ dict(cmd="fcm", fcm_id="FCM222", uuid="uuid"), dict(cmd="status", cc="US", dev="Nexus 5X") ]) response = self.client.post(reverse("register"), json.dumps(reg_data), content_type="application/json") self.assertEqual(response.status_code, 200) # channel country and device updated android2.refresh_from_db() self.assertEqual(android2.country, "US") self.assertEqual(android2.device, "Nexus 5X") self.assertEqual(android2.org, self.org) self.assertEqual(android2.config["FCM_ID"], "FCM222") self.assertEqual(android2.uuid, "uuid") self.assertTrue(android2.is_active) # set back to RW... android2.country = "RW" android2.save() # our country is RW self.assertEqual(self.org.get_country_code(), "RW") # remove nexmo nexmo.release() self.assertEqual(self.org.get_country_code(), "RW") # register another device with country as US reg_data = dict(cmds=[ dict(cmd="fcm", fcm_id="FCM444", uuid="uuid4"), dict(cmd="status", cc="US", dev="Nexus 6P") ]) response = self.client.post(reverse("register"), json.dumps(reg_data), content_type="application/json") claim_code = response.json()["cmds"][0]["relayer_claim_code"] # try to claim it... self.client.post( reverse("channels.types.android.claim"), dict(claim_code=claim_code, phone_number="12065551212")) # should work, can have two channels in different countries channel = Channel.objects.get(country="US") self.assertEqual(channel.address, "+12065551212") self.assertEqual( Channel.objects.filter(org=self.org, is_active=True).count(), 2) # normalize a URN with a fully qualified number number, valid = URN.normalize_number("+12061112222", None) self.assertTrue(valid) # not international format number, valid = URN.normalize_number("0788383383", None) self.assertFalse(valid) # get our send channel without a URN, should just default to last default_channel = self.org.get_send_channel(TEL_SCHEME) self.assertEqual(default_channel, channel) # get our send channel for a Rwandan URN rwanda_channel = self.org.get_send_channel( TEL_SCHEME, ContactURN.create(self.org, None, "tel:+250788383383")) self.assertEqual(rwanda_channel, android2) # and a US one us_channel = self.org.get_send_channel( TEL_SCHEME, ContactURN.create(self.org, None, "tel:+12065555353")) self.assertEqual(us_channel, channel) # a different country altogether should just give us the default us_channel = self.org.get_send_channel( TEL_SCHEME, ContactURN.create(self.org, None, "tel:+593997290044")) self.assertEqual(us_channel, channel) self.org = Org.objects.get(id=self.org.id) self.assertIsNone(self.org.get_country_code()) # yet another registration in rwanda reg_data = dict(cmds=[ dict(cmd="fcm", fcm_id="FCM555", uuid="uuid5"), dict(cmd="status", cc="RW", dev="Nexus 5") ]) response = self.client.post(reverse("register"), json.dumps(reg_data), content_type="application/json") claim_code = response.json()["cmds"][0]["relayer_claim_code"] # try to claim it with number taken by other Android channel response = self.client.post( reverse("channels.types.android.claim"), dict(claim_code=claim_code, phone_number="+250788123124")) self.assertFormError( response, "form", "phone_number", "Another channel has this number. Please remove that channel first." ) # create channel in another org Channel.create(self.org2, self.admin2, "RW", "A", "", "+250788382382") # can claim it with this number, and because it's a fully qualified RW number, doesn't matter that channel is US response = self.client.post( reverse("channels.types.android.claim"), dict(claim_code=claim_code, phone_number="+250788382382")) self.assertRedirect(response, reverse("public.public_welcome")) # should be added with RW as the country self.assertTrue( Channel.objects.get(address="+250788382382", country="RW", org=self.org))
def post(self, request, *args, **kwargs): from twilio.request_validator import RequestValidator from temba.flows.models import FlowSession signature = request.META.get("HTTP_X_TWILIO_SIGNATURE", "") url = "https://" + request.get_host() + "%s" % request.get_full_path() channel_uuid = kwargs.get("uuid") call_sid = self.get_param("CallSid") direction = self.get_param("Direction") status = self.get_param("CallStatus") to_number = self.get_param("To") to_country = self.get_param("ToCountry") from_number = self.get_param("From") # Twilio sometimes sends un-normalized numbers if to_number and not to_number.startswith( "+") and to_country: # pragma: no cover to_number, valid = URN.normalize_number(to_number, to_country) # see if it's a twilio call being initiated if to_number and call_sid and direction == "inbound" and status == "ringing": # find a channel that knows how to answer twilio calls channel = self.get_ringing_channel(uuid=channel_uuid) if not channel: response = VoiceResponse() response.say( "Sorry, there is no channel configured to take this call. Goodbye." ) response.hangup() return HttpResponse(str(response)) org = channel.org if self.get_channel_type( ) == "T" and not org.is_connected_to_twilio(): return HttpResponse("No Twilio account is connected", status=400) client = self.get_client(channel=channel) validator = RequestValidator(client.auth[1]) signature = request.META.get("HTTP_X_TWILIO_SIGNATURE", "") url = "https://%s%s" % (request.get_host(), request.get_full_path()) if validator.validate(url, request.POST, signature): from temba.ivr.models import IVRCall # find a contact for the one initiating us urn = URN.from_tel(from_number) contact, urn_obj = Contact.get_or_create( channel.org, urn, channel) flow = Trigger.find_flow_for_inbound_call(contact) if flow: call = IVRCall.create_incoming(channel, contact, urn_obj, channel.created_by, call_sid) session = FlowSession.create(contact, connection=call) call.update_status(request.POST.get("CallStatus", None), request.POST.get("CallDuration", None), "T") call.save() FlowRun.create(flow, contact, session=session, connection=call) response = Flow.handle_call(call) return HttpResponse(str(response)) else: # we don't have an inbound trigger to deal with this call. response = channel.generate_ivr_response() # say nothing and hangup, this is a little rude, but if we reject the call, then # they'll get a non-working number error. We send 'busy' when our server is down # so we don't want to use that here either. response.say("") response.hangup() # if they have a missed call trigger, fire that off Trigger.catch_triggers(contact, Trigger.TYPE_MISSED_CALL, channel) # either way, we need to hangup now return HttpResponse(str(response)) # check for call progress events, these include post-call hangup notifications if request.POST.get("CallbackSource", None) == "call-progress-events": if call_sid: from temba.ivr.models import IVRCall call = IVRCall.objects.filter(external_id=call_sid).first() if call: call.update_status(request.POST.get("CallStatus", None), request.POST.get("CallDuration", None), "TW") call.save() return HttpResponse("Call status updated") return HttpResponse("No call found") return HttpResponse("Not Handled, unknown action", status=400) # pragma: no cover