def from_ivr_call(cls, org: Org, user: User, obj: IVRCall) -> dict: logs_url = (_url_for_user( org, user, "channels.channellog_connection", args=[obj.id]) if obj.has_logs() else None) return { "type": cls.TYPE_CALL_STARTED, "created_on": get_event_time(obj).isoformat(), "status": obj.status, "status_display": obj.get_status_display(), "logs_url": logs_url, }
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 get(self, request, *args, **kwargs): from temba.flows.models import FlowSession from temba.ivr.models import IVRCall action = kwargs["action"].lower() request_body = force_text(request.body) request_path = request.get_full_path() request_method = request.method request_uuid = kwargs["uuid"] if action == "event": if not request_body: return HttpResponse("") body_json = json.loads(request_body) status = body_json.get("status", None) duration = body_json.get("duration", None) call_uuid = body_json.get("uuid", None) conversation_uuid = body_json.get("conversation_uuid", None) if call_uuid is None: return HttpResponse("Missing uuid parameter, ignoring") call = IVRCall.objects.filter(external_id=call_uuid).first() if not call: # try looking up by the conversation uuid (inbound calls start with that) call = IVRCall.objects.filter(external_id=conversation_uuid).first() if call: call.external_id = call_uuid call.save() else: response = dict(message="Call not found for %s" % call_uuid) return JsonResponse(response) channel = call.channel channel_type = channel.channel_type call.update_status(status, duration, channel_type) call.save() response = dict( description="Updated call status", call=dict(status=call.get_status_display(), duration=call.duration) ) event = HttpEvent(request_method, request_path, request_body, 200, json.dumps(response)) ChannelLog.log_ivr_interaction(call, "Updated call status", event) if call.status == IVRCall.COMPLETED: # if our call is completed, hangup runs = FlowRun.objects.filter(connection=call) for run in runs: if not run.is_completed(): run.set_completed(exit_uuid=None) return JsonResponse(response) if action == "answer": if not request_body: return HttpResponse("") body_json = json.loads(request_body) from_number = body_json.get("from", None) channel_number = body_json.get("to", None) external_id = body_json.get("conversation_uuid", None) if not from_number or not channel_number or not external_id: return HttpResponse("Missing parameters, Ignoring") # look up the channel address_q = Q(address=channel_number) | Q(address=("+" + channel_number)) channel = Channel.objects.filter(address_q).filter(is_active=True, channel_type="NX").first() # make sure we got one, and that it matches the key for our org org_uuid = None if channel: org_uuid = channel.org.config.get(NEXMO_UUID, None) if not channel or org_uuid != request_uuid: return HttpResponse("Channel not found for number: %s" % channel_number, status=404) urn = URN.from_tel(from_number) contact, urn_obj = Contact.get_or_create(channel.org, urn, channel) flow = Trigger.find_flow_for_inbound_call(contact) if flow: call = IVRCall.create_incoming(channel, contact, urn_obj, channel.created_by, external_id) session = FlowSession.create(contact, connection=call) FlowRun.create(flow, contact, session=session, connection=call) response = Flow.handle_call(call) channel_type = channel.channel_type call.update_status("answered", None, channel_type) event = HttpEvent(request_method, request_path, request_body, 200, str(response)) ChannelLog.log_ivr_interaction(call, "Incoming request for call", event) return JsonResponse(json.loads(str(response)), safe=False) else: # we don't have an inbound trigger to deal with this call. response = channel.generate_ivr_response() # say nothing and hangup, this is a little rude, but if we reject the call, then # they'll get a non-working number error. We send 'busy' when our server is down # so we don't want to use that here either. response.say("") response.hangup() # if they have a missed call trigger, fire that off Trigger.catch_triggers(contact, Trigger.TYPE_MISSED_CALL, channel) # either way, we need to hangup now return JsonResponse(json.loads(str(response)), safe=False)
def test_ivr_options(self): # 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.org.save() self.assertTrue(self.org.is_connected_to_twilio()) self.assertIsNotNone(self.org.get_twilio_client()) # 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.all().first().keyword) user_settings = self.admin.get_settings() user_settings.tel = '+18005551212' user_settings.save() # start our flow` eric = self.create_contact('Eric Newcomer', number='+13603621737') eric.is_test = True eric.save() Contact.set_simulation(True) flow.start([], [eric]) # should be using the usersettings number in test mode self.assertEquals('Placing test call to +1 800-555-1212', ActionLog.objects.all().first().text) # 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>' ) # updated our status and duration accordingly call = IVRCall.objects.get(pk=call.pk) self.assertEquals(20, call.duration) self.assertEquals(IN_PROGRESS, call.status) # should mention our our action log that we read a message to them run = FlowRun.objects.all().first() logs = ActionLog.objects.filter(run=run).order_by('-pk') self.assertEquals(2, len(logs)) self.assertEquals( 'Read message "Would you like me to call you? Press one for yes, two for no, or three for maybe."', logs.first().text) # press the number 4 (unexpected) response = self.client.post( reverse('ivr.ivrcall_handle', args=[call.pk]), dict(Digits=4)) self.assertContains(response, '<Say>Press one, two, or three. Thanks.</Say>') # 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>') # 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')) self.assertEquals(COMPLETED, IVRCall.objects.get(pk=call.pk).status) # simulation gets flipped off by middleware, and this unhandled message doesn't flip it back on self.assertFalse(Contact.get_simulation()) # 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) # explicitly hanging up an in progress call should remove it call.update_status('in-progress', 0) call.save() IVRCall.hangup_test_call(flow) self.assertIsNone(IVRCall.objects.filter(pk=call.pk).first())
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=IVRCall.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=IVRCall.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(IVRCall.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(IVRCall.COMPLETED, call.status) self.assertFalse(FlowRun.objects.filter(session=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', IVRCall.QUEUED) test_status_update(call, 'ringing', IVRCall.RINGING) test_status_update(call, 'canceled', IVRCall.CANCELED) test_status_update(call, 'busy', IVRCall.BUSY) test_status_update(call, 'failed', IVRCall.FAILED) test_status_update(call, 'no-answer', IVRCall.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=IVRCall.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=IVRCall.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=IVRCall.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())
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 get(self, request, *args, **kwargs): from temba.flows.models import FlowSession from temba.ivr.models import IVRCall action = kwargs["action"].lower() request_body = force_text(request.body) request_path = request.get_full_path() request_method = request.method request_uuid = kwargs["uuid"] if action == "event": if not request_body: return HttpResponse("") body_json = json.loads(request_body) status = body_json.get("status", None) duration = body_json.get("duration", None) call_uuid = body_json.get("uuid", None) conversation_uuid = body_json.get("conversation_uuid", None) if call_uuid is None: return HttpResponse("Missing uuid parameter, ignoring") call = IVRCall.objects.filter(external_id=call_uuid).first() if not call: # try looking up by the conversation uuid (inbound calls start with that) call = IVRCall.objects.filter( external_id=conversation_uuid).first() if call: call.external_id = call_uuid call.save() else: response = dict(message="Call not found for %s" % call_uuid) return JsonResponse(response) channel = call.channel channel_type = channel.channel_type call.update_status(status, duration, channel_type) call.save() response = dict(description="Updated call status", call=dict(status=call.get_status_display(), duration=call.duration)) event = HttpEvent(request_method, request_path, request_body, 200, json.dumps(response)) ChannelLog.log_ivr_interaction(call, "Updated call status", event) if call.status == IVRCall.COMPLETED: # if our call is completed, hangup runs = FlowRun.objects.filter(connection=call) for run in runs: if not run.is_completed(): run.set_completed(exit_uuid=None) return JsonResponse(response) if action == "answer": if not request_body: return HttpResponse("") body_json = json.loads(request_body) from_number = body_json.get("from", None) channel_number = body_json.get("to", None) external_id = body_json.get("conversation_uuid", None) if not from_number or not channel_number or not external_id: return HttpResponse("Missing parameters, Ignoring") # look up the channel address_q = Q(address=channel_number) | Q(address=("+" + channel_number)) channel = Channel.objects.filter(address_q).filter( is_active=True, channel_type="NX").first() # make sure we got one, and that it matches the key for our org org_uuid = None if channel: org_uuid = channel.org.config.get(NEXMO_UUID, None) if not channel or org_uuid != request_uuid: return HttpResponse("Channel not found for number: %s" % channel_number, status=404) urn = URN.from_tel(from_number) contact, urn_obj = Contact.get_or_create(channel.org, urn, channel) flow = Trigger.find_flow_for_inbound_call(contact) if flow: call = IVRCall.create_incoming(channel, contact, urn_obj, channel.created_by, external_id) session = FlowSession.create(contact, connection=call) FlowRun.create(flow, contact, session=session, connection=call) response = Flow.handle_call(call) channel_type = channel.channel_type call.update_status("answered", None, channel_type) event = HttpEvent(request_method, request_path, request_body, 200, str(response)) ChannelLog.log_ivr_interaction(call, "Incoming request for call", event) return JsonResponse(json.loads(str(response)), safe=False) else: # we don't have an inbound trigger to deal with this call. response = channel.generate_ivr_response() # say nothing and hangup, this is a little rude, but if we reject the call, then # they'll get a non-working number error. We send 'busy' when our server is down # so we don't want to use that here either. response.say("") response.hangup() # if they have a missed call trigger, fire that off Trigger.catch_triggers(contact, Trigger.TYPE_MISSED_CALL, channel) # either way, we need to hangup now return JsonResponse(json.loads(str(response)), safe=False)
def test_ivr_flow(self): # 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.org.save() self.assertTrue(self.org.is_connected_to_twilio()) self.assertIsNotNone(self.org.get_twilio_client()) # 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) # explicitly hanging up on a test call should remove it call.update_status('in-progress', 0) call.save() IVRCall.hangup_test_call(flow) self.assertIsNone(IVRCall.objects.filter(pk=call.pk).first()) ActionLog.objects.all().delete() IVRCall.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.all_messages.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) # press the number 4 (unexpected) response = self.client.post(reverse('ivr.ivrcall_handle', args=[call.pk]), dict(Digits=4)) self.assertContains(response, '<Say>Press one, two, or three. Thanks.</Say>') self.assertEquals(4, self.org.get_credits_used()) # two more messages, one inbound and it's response self.assertEquals(3, Msg.all_messages.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.all_messages.filter(msg_type=IVR).order_by('pk') self.assertEquals(5, messages.count()) self.assertEquals(6, 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()) # test invalid contact id with self.assertRaises(ValueError): IVRCall.create_outgoing(call.channel, 999, flow, self.admin) # test no valid urn with self.assertRaises(ValueError): call.contact.urns.all().delete() IVRCall.create_outgoing(call.channel, call.contact.pk, flow, self.admin) # 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())
def test_ivr_options(self): # 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.org.save() self.assertTrue(self.org.is_connected_to_twilio()) self.assertIsNotNone(self.org.get_twilio_client()) # 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.all().first().keyword) user_settings = self.admin.get_settings() user_settings.tel = '+18005551212' user_settings.save() # start our flow` eric = self.create_contact('Eric Newcomer', number='+13603621737') eric.is_test = True eric.save() Contact.set_simulation(True) flow.start([], [eric]) # should be using the usersettings number in test mode self.assertEquals('Placing test call to +1 800-555-1212', ActionLog.objects.all().first().text) # 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>') # updated our status and duration accordingly call = IVRCall.objects.get(pk=call.pk) self.assertEquals(20, call.duration) self.assertEquals(IN_PROGRESS, call.status) # should mention our our action log that we read a message to them run = FlowRun.objects.all().first() logs = ActionLog.objects.filter(run=run).order_by('-pk') self.assertEquals(2, len(logs)) self.assertEquals('Read message "Would you like me to call you? Press one for yes, two for no, or three for maybe."', logs.first().text) # press the number 4 (unexpected) response = self.client.post(reverse('ivr.ivrcall_handle', args=[call.pk]), dict(Digits=4)) self.assertContains(response, '<Say>Press one, two, or three. Thanks.</Say>') # 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>') # 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')) self.assertEquals(COMPLETED, IVRCall.objects.get(pk=call.pk).status) # simulation gets flipped off by middleware, and this unhandled message doesn't flip it back on self.assertFalse(Contact.get_simulation()) # 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) # explicitly hanging up an in progress call should remove it call.update_status('in-progress', 0) call.save() IVRCall.hangup_test_call(flow) self.assertIsNone(IVRCall.objects.filter(pk=call.pk).first())