def fire_follow_triggers(channel_id, contact_urn_id, new_mage_contact=False): """ Fires a follow trigger """ urn = ContactURN.objects.select_related('contact').get(pk=contact_urn_id) contact = urn.contact # for now, flows start against contacts rather than URNs # if contact was just created in Mage then.. # * its dynamic groups won't have been initialized # * we need to update our cached contact counts if new_mage_contact: mage_handle_new_contact(contact.org, contact) Trigger.catch_triggers(contact, FOLLOW_TRIGGER, channel_id=channel_id)
def test_new_conversation_triggers(self, mock_post): mock_post.return_value = MockResponse(200, json.dumps({'success': True})) flow = self.create_flow() trigger = Trigger.create(self.org, self.admin, Trigger.TYPE_NEW_CONVERSATION, flow, self.channel) mock_post.assert_called_once_with('https://graph.facebook.com/v2.6/12345/thread_settings', json={ 'setting_type': 'call_to_actions', 'thread_state': 'new_thread', 'call_to_actions': [{"payload": "get_started"}] }, headers={'Content-Type': 'application/json'}, params={'access_token': '09876543'}) mock_post.reset_mock() trigger.archive(self.admin) mock_post.assert_called_once_with('https://graph.facebook.com/v2.6/12345/thread_settings', json={ 'setting_type': 'call_to_actions', 'thread_state': 'new_thread', 'call_to_actions': [] }, headers={'Content-Type': 'application/json'}, params={'access_token': '09876543'}) mock_post.reset_mock() trigger.restore(self.admin) mock_post.assert_called_once_with('https://graph.facebook.com/v2.6/12345/thread_settings', json={ 'setting_type': 'call_to_actions', 'thread_state': 'new_thread', 'call_to_actions': [{"payload": "get_started"}] }, headers={'Content-Type': 'application/json'}, params={'access_token': '09876543'}) mock_post.reset_mock()
def test_new_conversation_triggers(self): flow = self.create_flow() with patch("requests.post") as mock_post: mock_post.return_value = MockResponse( 200, json.dumps({"success": True})) trigger = Trigger.create(self.org, self.admin, Trigger.TYPE_NEW_CONVERSATION, flow, channel=self.channel) mock_post.assert_called_once_with( "https://graph.facebook.com/v3.3/12345/thread_settings", json={ "setting_type": "call_to_actions", "thread_state": "new_thread", "call_to_actions": [{ "payload": "get_started" }], }, headers={"Content-Type": "application/json"}, params={"access_token": "09876543"}, ) mock_post.reset_mock() trigger.archive(self.admin) mock_post.assert_called_once_with( "https://graph.facebook.com/v3.3/12345/thread_settings", json={ "setting_type": "call_to_actions", "thread_state": "new_thread", "call_to_actions": [] }, headers={"Content-Type": "application/json"}, params={"access_token": "09876543"}, ) mock_post.reset_mock() trigger.restore(self.admin) mock_post.assert_called_once_with( "https://graph.facebook.com/v3.3/12345/thread_settings", json={ "setting_type": "call_to_actions", "thread_state": "new_thread", "call_to_actions": [{ "payload": "get_started" }], }, headers={"Content-Type": "application/json"}, params={"access_token": "09876543"}, ) mock_post.reset_mock()
def send(self, message, contact=None): if not contact: contact = self.contact if contact.is_test: Contact.set_simulation(True) incoming = self.create_msg(direction=INCOMING, contact=contact, text=message) # evaluate the inbound message against our triggers first from temba.triggers.models import Trigger if not Trigger.find_and_handle(incoming): Flow.find_and_handle(incoming) return Msg.objects.filter(response_to=incoming).order_by('pk').first()
def test_new_conversation_triggers(self): flow = self.create_flow() with patch("requests.post") as mock_post: mock_post.return_value = MockResponse( 200, json.dumps({"success": True})) trigger = Trigger.create(self.org, self.admin, Trigger.TYPE_NEW_CONVERSATION, flow, channel=self.channel) mock_post.assert_called_once_with( "https://graph.facebook.com/v12.0/me/messenger_profile", json={"get_started": { "payload": "get_started" }}, headers={"Content-Type": "application/json"}, params={"access_token": "09876543"}, ) mock_post.reset_mock() with patch("requests.delete") as mock_post: mock_post.return_value = MockResponse( 200, json.dumps({"success": True})) trigger.archive(self.admin) mock_post.assert_called_once_with( "https://graph.facebook.com/v12.0/me/messenger_profile", json={"fields": ["get_started"]}, headers={"Content-Type": "application/json"}, params={"access_token": "09876543"}, ) mock_post.reset_mock() with patch("requests.post") as mock_post: mock_post.return_value = MockResponse( 200, json.dumps({"success": True})) trigger.restore(self.admin) mock_post.assert_called_once_with( "https://graph.facebook.com/v12.0/me/messenger_profile", json={"get_started": { "payload": "get_started" }}, headers={"Content-Type": "application/json"}, params={"access_token": "09876543"}, ) mock_post.reset_mock()
def handle_message(msg): """ Only used for testing to approximate how mailroom handles a message """ from temba.flows.models import Flow from temba.msgs.models import Msg from temba.triggers.models import Trigger if msg.contact.is_blocked: msg.visibility = Msg.VISIBILITY_ARCHIVED msg.save(update_fields=["visibility", "modified_on"]) else: handled = Trigger.find_and_handle(msg) if not handled: handled, msgs = Flow.find_and_handle(msg) if not handled: Trigger.catch_triggers(msg, Trigger.TYPE_CATCH_ALL, msg.channel) mark_handled(msg)
def test_new_conversation_triggers(self, mock_post): mock_post.return_value = MockResponse(200, json.dumps({"success": True})) flow = self.create_flow() trigger = Trigger.create(self.org, self.admin, Trigger.TYPE_NEW_CONVERSATION, flow, self.channel) mock_post.assert_called_once_with( "https://graph.facebook.com/v2.12/12345/thread_settings", json={ "setting_type": "call_to_actions", "thread_state": "new_thread", "call_to_actions": [{"payload": "get_started"}], }, headers={"Content-Type": "application/json"}, params={"access_token": "09876543"}, ) mock_post.reset_mock() trigger.archive(self.admin) mock_post.assert_called_once_with( "https://graph.facebook.com/v2.12/12345/thread_settings", json={"setting_type": "call_to_actions", "thread_state": "new_thread", "call_to_actions": []}, headers={"Content-Type": "application/json"}, params={"access_token": "09876543"}, ) mock_post.reset_mock() trigger.restore(self.admin) mock_post.assert_called_once_with( "https://graph.facebook.com/v2.12/12345/thread_settings", json={ "setting_type": "call_to_actions", "thread_state": "new_thread", "call_to_actions": [{"payload": "get_started"}], }, headers={"Content-Type": "application/json"}, params={"access_token": "09876543"}, ) mock_post.reset_mock()
def setUpBeforeMigration(self, apps): contact1 = self.create_contact("Bob", twitter="bob") favorites = self.get_flow("favorites") # create schedule attached to a trigger self.trigger = Trigger.create(self.org, self.admin, Trigger.TYPE_SCHEDULE, flow=favorites, schedule=create_schedule( self.admin, "D")) # create schedule attached to a broadcast self.broadcast = Broadcast.create(self.org, self.admin, "hi there", contacts=[contact1], schedule=create_schedule( self.admin, "W")) # create orphan schedule create_schedule(self.admin, "M")
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)
class USSDSession(ChannelSession): USSD_PULL = INCOMING = 'I' USSD_PUSH = OUTGOING = 'O' objects = USSDQuerySet.as_manager() class Meta: proxy = True @property def should_end(self): return self.status == self.ENDING def mark_ending(self): # session to be ended if self.status != self.ENDING: self.status = self.ENDING self.save(update_fields=['status']) def close(self): # session has successfully ended if self.status == self.ENDING: self.status = self.COMPLETED else: self.status = self.INTERRUPTED self.ended_on = timezone.now() self.save(update_fields=['status', 'ended_on']) def start_async(self, flow, date, message_id): from temba.msgs.models import Msg, USSD message = Msg.objects.create(channel=self.channel, contact=self.contact, contact_urn=self.contact_urn, sent_on=date, connection=self, msg_type=USSD, external_id=message_id, created_on=timezone.now(), modified_on=timezone.now(), org=self.channel.org, direction=self.INCOMING) flow.start([], [self.contact], start_msg=message, restart_participants=True, connection=self) def handle_async(self, urn, content, date, message_id): from temba.msgs.models import Msg, USSD Msg.create_incoming(channel=self.channel, org=self.org, urn=urn, text=content or '', date=date, connection=self, msg_type=USSD, external_id=message_id) def handle_sync(self): # pragma: needs cover # TODO: implement for InfoBip and other sync APIs pass @classmethod def handle_incoming(cls, channel, urn, date, external_id, contact=None, message_id=None, status=None, content=None, starcode=None, org=None, async=True): trigger = None contact_urn = None # handle contact with channel urn = URN.from_tel(urn) if not contact: contact, contact_urn = Contact.get_or_create( channel.org, urn, channel) elif urn: contact_urn = ContactURN.get_or_create(org, contact, urn, channel=channel) contact.set_preferred_channel(channel) if contact_urn: contact_urn.update_affinity(channel) # setup session defaults = dict(channel=channel, contact=contact, contact_urn=contact_urn, org=channel.org if channel else contact.org) if status == cls.TRIGGERED: trigger = Trigger.find_trigger_for_ussd_session(contact, starcode) if not trigger: return False defaults.update( dict(started_on=date, direction=cls.USSD_PULL, status=status)) elif status == cls.INTERRUPTED: defaults.update(dict(ended_on=date, status=status)) else: defaults.update(dict(status=cls.IN_PROGRESS)) # check if there's an initiated PUSH connection connection = cls.objects.get_initiated_push(contact) created = False if not connection: try: connection = cls.objects.select_for_update().exclude(status__in=ChannelSession.DONE)\ .get(external_id=external_id) created = False for k, v in six.iteritems(defaults): setattr(connection, k, v() if callable(v) else v) connection.save() except cls.DoesNotExist: defaults['external_id'] = external_id connection = cls.objects.create(**defaults) FlowSession.create(contact, connection=connection) created = True else: defaults.update(dict(external_id=external_id)) for key, value in six.iteritems(defaults): setattr(connection, key, value) connection.save() created = None # start session if created and async and trigger: connection.start_async(trigger.flow, date, message_id)
class USSDSession(ChannelSession): USSD_PULL = INCOMING = 'I' USSD_PUSH = OUTGOING = 'O' objects = USSDQuerySet.as_manager() class Meta: proxy = True def start_session_async(self, flow, urn, content, date, message_id): from temba.msgs.models import Msg, USSD message = Msg.objects.create(channel=self.channel, contact=self.contact, contact_urn=self.contact_urn, sent_on=date, session=self, msg_type=USSD, external_id=message_id, created_on=timezone.now(), modified_on=timezone.now(), org=self.channel.org, direction=self.INCOMING) flow.start([], [self.contact], start_msg=message, restart_participants=True, session=self) def handle_session_async(self, urn, content, date, message_id): from temba.msgs.models import Msg, USSD Msg.create_incoming(channel=self.channel, urn=urn, text=content or '', date=date, session=self, msg_type=USSD, external_id=message_id) def handle_ussd_session_sync(self): # pragma: needs cover # TODO: implement for InfoBip and other sync APIs pass @classmethod def handle_incoming(cls, channel, urn, date, external_id, message_id=None, status=None, flow=None, content=None, starcode=None, org=None, async=True): trigger = None # handle contact with channel urn = URN.from_tel(urn) contact = Contact.get_or_create(channel.org, channel.created_by, urns=[urn], channel=channel) contact_urn = contact.urn_objects[urn] contact.set_preferred_channel(channel) contact_urn.update_affinity(channel) # setup session defaults = dict(channel=channel, contact=contact, contact_urn=contact_urn, org=channel.org) if status == cls.TRIGGERED: trigger = Trigger.find_trigger_for_ussd_session(contact, starcode) if not trigger: return False defaults.update( dict(started_on=date, direction=cls.USSD_PULL, status=status)) elif status == cls.INTERRUPTED: defaults.update(dict(ended_on=date, status=status)) else: defaults.update(dict(status=USSDSession.IN_PROGRESS)) # check if there's an initiated PUSH session session = cls.objects.get_initiated_push_session(contact) if not session: session, created = cls.objects.update_or_create( external_id=external_id, defaults=defaults) else: defaults.update(dict(external_id=external_id)) for key, value in six.iteritems(defaults): setattr(session, key, value) session.save() created = None # start session if created and async and trigger: session.start_session_async(trigger.flow, urn, content, date, message_id)
def handle_incoming( cls, channel, urn, date, external_id, contact=None, message_id=None, status=None, content=None, starcode=None, org=None, do_async=True, ): trigger = None contact_urn = None # handle contact with channel urn = URN.from_tel(urn) if not contact: contact, contact_urn = Contact.get_or_create(channel.org, urn, channel) elif urn: contact_urn = ContactURN.get_or_create(org, contact, urn, channel=channel) contact.set_preferred_channel(channel) if contact_urn: contact_urn.update_affinity(channel) # setup session defaults = dict( channel=channel, contact=contact, contact_urn=contact_urn, org=channel.org if channel else contact.org ) if status == cls.TRIGGERED: trigger = Trigger.find_trigger_for_ussd_session(contact, starcode) if not trigger: return False defaults.update(dict(started_on=date, direction=cls.USSD_PULL, status=status)) elif status == cls.INTERRUPTED: defaults.update(dict(ended_on=date, status=status)) else: defaults.update(dict(status=cls.IN_PROGRESS)) # check if there's an initiated PUSH connection connection = cls.objects.get_initiated_push(contact) created = False if not connection: try: connection = ( cls.objects.select_for_update() .exclude(status__in=ChannelConnection.DONE) .get(external_id=external_id) ) created = False for k, v in defaults.items(): setattr(connection, k, v() if callable(v) else v) connection.save() except cls.DoesNotExist: defaults["external_id"] = external_id connection = cls.objects.create(**defaults) FlowSession.create(contact, connection=connection) created = True else: defaults.update(dict(external_id=external_id)) for key, value in defaults.items(): setattr(connection, key, value) connection.save() created = None # start session if created and do_async and trigger: connection.start_async(trigger.flow, date, message_id) # resume session, deal with incoming content and all the other states else: connection.handle_async(urn, content, date, message_id) return connection
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_object_class_plural(self): self.assertEqual("Flow", object_class_name(Flow())) self.assertEqual("Campaign", object_class_name(Campaign())) self.assertEqual("CampaignEvent", object_class_name(CampaignEvent())) self.assertEqual("Trigger", object_class_name(Trigger()))
def handle_incoming( cls, channel, urn, date, external_id, contact=None, message_id=None, status=None, content=None, starcode=None, org=None, do_async=True, ): trigger = None contact_urn = None # handle contact with channel urn = URN.from_tel(urn) if not contact: contact, contact_urn = Contact.get_or_create( channel.org, urn, channel) elif urn: contact_urn = ContactURN.get_or_create(org, contact, urn, channel=channel) contact.set_preferred_channel(channel) if contact_urn: contact_urn.update_affinity(channel) # setup session defaults = dict(channel=channel, contact=contact, contact_urn=contact_urn, org=channel.org if channel else contact.org) if status == cls.TRIGGERED: trigger = Trigger.find_trigger_for_ussd_session(contact, starcode) if not trigger: return False defaults.update( dict(started_on=date, direction=cls.USSD_PULL, status=status)) elif status == cls.INTERRUPTED: defaults.update(dict(ended_on=date, status=status)) else: defaults.update(dict(status=cls.IN_PROGRESS)) # check if there's an initiated PUSH connection connection = cls.objects.get_initiated_push(contact) created = False if not connection: try: connection = (cls.objects.select_for_update().exclude( status__in=ChannelSession.DONE).get( external_id=external_id)) created = False for k, v in defaults.items(): setattr(connection, k, v() if callable(v) else v) connection.save() except cls.DoesNotExist: defaults["external_id"] = external_id connection = cls.objects.create(**defaults) FlowSession.create(contact, connection=connection) created = True else: defaults.update(dict(external_id=external_id)) for key, value in defaults.items(): setattr(connection, key, value) connection.save() created = None # start session if created and do_async and trigger: connection.start_async(trigger.flow, date, message_id) # resume session, deal with incoming content and all the other states else: connection.handle_async(urn, content, date, message_id) return connection