def refresh_access_token(self, channel_id): r = get_redis_connection() lock_name = self.TOKEN_REFRESH_LOCK % self.channel_uuid if not r.get(lock_name): with r.lock(lock_name, timeout=30): key = self.TOKEN_STORE_KEY % self.channel_uuid post_data = dict(grant_type="client_credentials", client_id=self.app_id, client_secret=self.app_secret) url = self.TOKEN_URL event = HttpEvent("POST", url, json.dumps(post_data)) start = time.time() response = self._request(url, post_data, access_token=None) event.status_code = response.status_code if response.status_code != 200: event.response_body = response.content ChannelLog.log_channel_request( channel_id, "Got non-200 response from %s" % self.API_NAME, event, start, True ) return response_json = response.json() event.response_body = json.dumps(response_json) ChannelLog.log_channel_request( channel_id, "Successfully fetched access token from %s" % self.API_NAME, event, start ) access_token = response_json["access_token"] expires = response_json.get("expires_in", 7200) r.set(key, access_token, ex=int(expires)) return access_token
def check_calls_task(): from .models import IVRCall now = timezone.now() calls_to_retry = ( IVRCall.objects.filter(next_attempt__lte=now, retry_count__lte=IVRCall.MAX_RETRY_ATTEMPTS) .filter(status__in=IVRCall.RETRY_CALL) .filter(modified_on__gt=now - timedelta(days=IVRCall.IGNORE_PENDING_CALLS_OLDER_THAN_DAYS)) .filter(direction=IVRCall.OUTGOING, is_active=True) ) for call in calls_to_retry: ChannelLog.log_ivr_interaction(call, "Retrying call", HttpEvent(method="INTERNAL", url=None)) call.status = IVRCall.PENDING call.next_attempt = None # reset the call call.started_on = None call.ended_on = None call.duration = 0 call.modified_on = timezone.now() call.save(update_fields=("status", "next_attempt", "started_on", "ended_on", "duration", "modified_on")) if calls_to_retry: task_enqueue_call_events.apply_async()
def check_calls_task(): from .models import IVRCall now = timezone.now() calls_to_retry = (IVRCall.objects.filter( next_attempt__lte=now, retry_count__lte=IVRCall.MAX_RETRY_ATTEMPTS).filter( status__in=IVRCall.RETRY_CALL).filter( modified_on__gt=now - timedelta( days=IVRCall.IGNORE_PENDING_CALLS_OLDER_THAN_DAYS)).filter( direction=IVRCall.OUTGOING)) for call in calls_to_retry: ChannelLog.log_ivr_interaction(call, "Retrying call", HttpEvent(method="INTERNAL", url=None)) call.status = IVRCall.PENDING call.next_attempt = None # reset the call call.started_on = None call.ended_on = None call.duration = 0 call.modified_on = timezone.now() call.save(update_fields=("status", "next_attempt", "started_on", "ended_on", "duration", "modified_on")) if calls_to_retry: task_enqueue_call_events.apply_async()
def check_failed_calls_task(): from .models import IVRCall # calls that have failed and have a `error_count` value are going to be retried failed_calls_to_retry = (IVRCall.objects.filter( error_count__gte=1, error_count__lte=IVRCall.MAX_ERROR_COUNT).filter( status__in=IVRCall.FAILED).filter( modified_on__gt=timezone.now() - timedelta( days=IVRCall.IGNORE_PENDING_CALLS_OLDER_THAN_DAYS)).filter( direction=IVRCall.OUTGOING)) for call in failed_calls_to_retry: ChannelLog.log_ivr_interaction(call, "Retrying failed call", HttpEvent(method="INTERNAL", url=None)) call.status = IVRCall.PENDING # reset the call call.started_on = None call.ended_on = None call.duration = 0 call.modified_on = timezone.now() call.save(update_fields=("status", "next_attempt", "started_on", "ended_on", "duration", "modified_on")) if failed_calls_to_retry: task_enqueue_call_events.apply_async()
def check_calls_task(): from .models import IVRCall now = timezone.now() calls_to_retry = ( IVRCall.objects.filter(next_attempt__lte=now, retry_count__lte=IVRCall.MAX_RETRY_ATTEMPTS) .filter(status__in=IVRCall.RETRY_CALL) .filter(direction=IVRCall.OUTGOING, is_active=True) ) for call in calls_to_retry: ChannelLog.log_ivr_interaction(call, "Retrying call", HttpEvent(method="INTERNAL", url=None)) call.status = IVRCall.QUEUED call.next_attempt = None # reset the call call.started_on = None call.ended_on = None call.duration = 0 call.modified_on = timezone.now() call.save() start_call_task.apply_async(kwargs={"call_pk": call.id}, queue="handler")
def handle_direct_inbound(self, request, uuid, data): from warapidpro.types import WhatsAppDirectType channel = self.lookup_channel(WhatsAppDirectType.code, uuid) if not channel: error_msg = "Channel not found for id: %s" % (uuid, ) logger.error(error_msg) return HttpResponse(error_msg, status=400) from_addr = data['from_addr'] content = self.get_content(data) attachments = self.get_attachments(data) message = Msg.create_incoming(channel, URN.from_tel(from_addr), content, external_id=data['uuid'], attachments=attachments) response_body = { 'message_id': message.pk, } request_body = request.body request_method = request.method request_path = request.get_full_path() event = HttpEvent(request_method, request_path, request_body, 201, json.dumps(response_body)) ChannelLog.log_message(message, 'Handled inbound message.', event) return JsonResponse(response_body, status=201)
def refresh_access_token(self, channel_id): r = get_redis_connection() lock_name = JIOCHAT_ACCESS_TOKEN_REFRESH_LOCK % self.channel_uuid if not r.get(lock_name): with r.lock(lock_name, timeout=30): key = JIOCHAT_ACCESS_TOKEN_KEY % self.channel_uuid post_data = dict(grant_type="client_credentials", client_id=self.app_id, client_secret=self.app_secret) url = "https://channels.jiochat.com/auth/token.action" event = HttpEvent("POST", url, json.dumps(post_data)) start = time.time() response = self._request(url, post_data, access_token=None) event.status_code = response.status_code if response.status_code != 200: event.response_body = response.content ChannelLog.log_channel_request(channel_id, "Got non-200 response from Jiochat", event, start, True) return response_json = response.json() event.response_body = json.dumps(response_json) ChannelLog.log_channel_request( channel_id, "Successfully fetched access token from Jiochat", event, start ) access_token = response_json["access_token"] r.set(key, access_token, ex=7200) return access_token
def start_call(self, call, to, from_, status_callback): if not settings.SEND_CALLS: raise ValueError("SEND_CALLS set to False, skipping call start") params = dict(to=to, from_=call.channel.address, url=status_callback, status_callback=status_callback) try: twilio_call = self.api.calls.create(**params) call.external_id = str(twilio_call.sid) # the call was successfully sent to the IVR provider call.status = IVRCall.WIRED call.save() for event in self.events: ChannelLog.log_ivr_interaction(call, "Started call", event) except TwilioRestException as twilio_error: message = "Twilio Error: %s" % twilio_error.msg if twilio_error.code == 20003: message = _("Could not authenticate with your Twilio account. Check your token and try again.") event = HttpEvent("POST", "https://api.nexmo.com/v1/calls", json.dumps(params), response_body=str(message)) ChannelLog.log_ivr_interaction(call, "Call start failed", event, is_error=True) call.status = IVRCall.FAILED call.save() raise IVRException(message)
def start_call(self, call, to, from_, status_callback): if not settings.SEND_CALLS: raise IVRException("SEND_CALLS set to False, skipping call start") try: #time.sleep(60) sleeptime = settings.SLEEP_TIME_CALL #if sleeptime>0: #time.sleep(sleeptime) print("Wait for %s seconde before every call" % sleeptime) twilio_call = self.calls.create(to=to, from_=call.channel.address, url=status_callback, status_callback=status_callback) call.external_id = six.text_type(twilio_call.sid) call.save() for event in self.calls.events: ChannelLog.log_ivr_interaction(call, 'Started call', event) if sleeptime > 0: time.sleep(sleeptime) except TwilioRestException as twilio_error: message = 'Twilio Error: %s' % twilio_error.msg if twilio_error.code == 20003: message = _( 'Could not authenticate with your Twilio account. Check your token and try again.' ) raise IVRException(message)
def start_call(self, call, to, from_, status_callback): if not settings.SEND_CALLS: raise ValueError("SEND_CALLS set to False, skipping call start") url = "https://%s%s" % (self.org.get_brand_domain(), reverse("ivr.ivrcall_handle", args=[call.pk])) params = dict() params["answer_url"] = [url] params["answer_method"] = "POST" params["to"] = [dict(type="phone", number=to.strip("+"))] params["from"] = dict(type="phone", number=from_.strip("+")) params["event_url"] = ["%s?has_event=1" % url] params["event_method"] = "POST" try: response = self.create_call(params=params) call_uuid = response.get("uuid", None) call.external_id = str(call_uuid) # the call was successfully sent to the IVR provider call.status = IVRCall.WIRED call.save() for event in self.events: ChannelLog.log_ivr_interaction(call, "Started call", event) except Exception as e: event = HttpEvent("POST", "https://api.nexmo.com/v1/calls", json.dumps(params), response_body=str(e)) ChannelLog.log_ivr_interaction(call, "Call start failed", event, is_error=True) call.status = IVRCall.FAILED call.save() raise IVRException(_("Nexmo call failed, with error %s") % str(e))
def download_media(self, call, media_url): """ Fetches the recording and stores it with the provided recording_id :param media_url: the url where the media lives :return: the url for our downloaded media with full content type prefix """ attempts = 0 response = None while attempts < 4: response = self.download_recording(media_url) # in some cases Twilio isn't ready for us to fetch the recording URL yet, if we get a 404 # sleep for a bit then try again up to 4 times if response.status_code == 200: break else: attempts += 1 time.sleep(0.250) content_type, downloaded = self.org.save_response_media(response) if content_type: # log that we downloaded it to our own url request = response.request event = HttpEvent(request.method, request.url, request.body, response.status_code, downloaded) ChannelLog.log_ivr_interaction(call, "Downloaded media", event) return "%s:%s" % (content_type, downloaded) return None
def get_user_detail(self, open_id, channel_id): access_token = self.get_access_token() url = 'https://channels.jiochat.com/user/info.action?' payload = dict(openid=open_id) event = HttpEvent('GET', url, json.dumps(payload)) start = time.time() response = self._request(url, 'GET', payload, access_token) event.status_code = response.status_code if response.status_code != 200: event.response_body = response.content ChannelLog.log_channel_request( channel_id, "Got non-200 response from Jiochat", event, start, True) return dict() data = response.json() event.response_body = json.dumps(data) ChannelLog.log_channel_request( channel_id, "Successfully fetched user detail from Jiochat", event, start) return data
def check_failed_calls_task(): from .models import IVRCall # calls that have failed and have a `error_count` value are going to be retried failed_calls_to_retry = ( IVRCall.objects.filter(error_count__gte=1, error_count__lte=IVRCall.MAX_ERROR_COUNT) .filter(status__in=IVRCall.FAILED) .filter(modified_on__gt=timezone.now() - timedelta(days=IVRCall.IGNORE_PENDING_CALLS_OLDER_THAN_DAYS)) .filter(direction=IVRCall.OUTGOING, is_active=True) ) for call in failed_calls_to_retry: ChannelLog.log_ivr_interaction(call, "Retrying failed call", HttpEvent(method="INTERNAL", url=None)) call.status = IVRCall.PENDING # reset the call call.started_on = None call.ended_on = None call.duration = 0 call.modified_on = timezone.now() call.save(update_fields=("status", "next_attempt", "started_on", "ended_on", "duration", "modified_on")) if failed_calls_to_retry: task_enqueue_call_events.apply_async()
def start_call(self, call, to, from_, status_callback): url = 'https://%s%s' % (settings.TEMBA_HOST, reverse('ivr.ivrcall_handle', args=[call.pk])) params = dict() params['answer_url'] = [url] params['answer_method'] = 'POST' params['to'] = [dict(type='phone', number=to.strip('+'))] params['from'] = dict(type='phone', number=from_.strip('+')) params['event_url'] = ["%s?has_event=1" % url] params['event_method'] = "POST" try: response = self.create_call(params=params) call_uuid = response.get('uuid', None) call.external_id = six.text_type(call_uuid) call.save() for event in self.events: ChannelLog.log_ivr_interaction(call, 'Started call', event) except Exception as e: event = HttpEvent('POST', 'https://api.nexmo.com/v1/calls', json.dumps(params), response_body=six.text_type(e)) ChannelLog.log_ivr_interaction(call, 'Call start failed', event, is_error=True) call.status = IVRCall.FAILED call.save() raise IVRException( _("Nexmo call failed, with error %s") % six.text_type(e.message))
def request_media(self, media_id, channel_id): access_token = self.get_access_token() url = 'https://channels.jiochat.com/media/download.action' payload = dict(media_id=media_id) event = HttpEvent('GET', url, json.dumps(payload)) start = time.time() response = None attempts = 0 while attempts < 4: response = self._request(url, 'GET', payload, access_token) # If we fail sleep for a bit then try again up to 4 times if response.status_code == 200: break else: attempts += 1 time.sleep(.250) if response: event.status_code = response.status_code if not event.status_code or event.status_code != 200: ChannelLog.log_channel_request(channel_id, "Failed to get media from Jiochat", event, start, True) else: ChannelLog.log_channel_request(channel_id, "Successfully fetched media from Jiochat", event, start) return response
def test_channellog(self): contact = self.create_contact("Test", "+250788383383") msg = Msg.create_outgoing(self.org, self.admin, contact, "This is a test message") msg = dict_to_struct('MockMsg', msg.as_task_json()) with SegmentProfiler("Channel Log inserts (10,000)", self, force_profile=True): for i in range(10000): ChannelLog.log_success(msg, "Sent Message", method="GET", url="http://foo", request="GET http://foo", response="Ok", response_status="201")
def start_call(self): from temba.ivr.tasks import start_call_task ChannelLog.log_ivr_interaction(self, "Call queued internally", HttpEvent(method="INTERNAL", url=None)) self.status = IVRCall.QUEUED self.save() on_transaction_commit(lambda: start_call_task.delay(self.pk))
def do_start_call(self, qs=None): client = self.channel.get_ivr_client() domain = self.channel.callback_domain from temba.ivr.clients import IVRException from temba.flows.models import ActionLog, FlowRun if client: try: url = "https://%s%s" % (domain, reverse("ivr.ivrcall_handle", args=[self.pk])) if qs: # pragma: no cover url = "%s?%s" % (url, qs) tel = None # if we are working with a test contact, look for user settings if self.contact.is_test: user_settings = self.created_by.get_settings() if user_settings.tel: tel = user_settings.tel run = FlowRun.objects.filter(connection=self) if run: ActionLog.create(run[0], "Placing test call to %s" % user_settings.get_tel_formatted()) if not tel: tel_urn = self.contact_urn tel = tel_urn.path client.start_call(self, to=tel, from_=self.channel.address, status_callback=url) except IVRException as e: import traceback traceback.print_exc() if self.contact.is_test: run = FlowRun.objects.filter(connection=self) ActionLog.create(run[0], "Call ended. %s" % str(e)) except Exception as e: # pragma: no cover import traceback traceback.print_exc() ChannelLog.log_ivr_interaction( self, "Call failed unexpectedly", HttpEvent(method="INTERNAL", url=None, response_body=str(e)) ) self.status = self.FAILED self.save() if self.contact.is_test: run = FlowRun.objects.filter(connection=self) ActionLog.create(run[0], "Call ended.")
def download_media(self, call, media_url): """ Fetches the recording and stores it with the provided recording_id :param media_url: the url where the media lives :return: the url for our downloaded media with full content type prefix """ attempts = 0 while attempts < 4: response = self.download_recording(media_url) # in some cases Twilio isn't ready for us to fetch the recording URL yet, if we get a 404 # sleep for a bit then try again up to 4 times if response.status_code == 200: break else: attempts += 1 time.sleep(.250) disposition = response.headers.get('Content-Disposition', None) content_type = response.headers.get('Content-Type', None) if content_type: extension = None if disposition == 'inline': extension = mimetypes.guess_extension(content_type) extension = extension.strip('.') elif disposition: filename = re.findall("filename=\"(.+)\"", disposition)[0] extension = filename.rpartition('.')[2] elif content_type == 'audio/x-wav': extension = 'wav' temp = NamedTemporaryFile(delete=True) temp.write(response.content) temp.flush() # save our file off downloaded = self.org.save_media(File(temp), extension) # log that we downloaded it to our own url request = response.request event = HttpEvent(request.method, request.url, request.body, response.status_code, downloaded) ChannelLog.log_ivr_interaction(call, "Downloaded media", event) return '%s:%s' % (content_type, downloaded) return None
def refresh_access_token(self, channel_id): r = get_redis_connection() lock_name = self.TOKEN_REFRESH_LOCK % self.channel_uuid if not r.get(lock_name): with r.lock(lock_name, timeout=30): key = self.TOKEN_STORE_KEY % self.channel_uuid data = dict(grant_type="client_credential", appid=self.app_id, secret=self.app_secret) url = self.TOKEN_URL event = HttpEvent("GET", url + "?" + urlencode(data)) start = time.time() response = requests.get(url, params=data, timeout=15) event.status_code = response.status_code if response.status_code != 200: event.response_body = response.content ChannelLog.log_channel_request( channel_id, "Got non-200 response from %s" % self.API_NAME, event, start, True) return response_json = response.json() has_error = False if response_json.get("errcode", -1) != 0: has_error = True event.response_body = json.dumps(response_json) ChannelLog.log_channel_request( channel_id, "Successfully fetched access token from %s" % self.API_NAME, event, start, has_error) access_token = response_json.get("access_token", "") expires = response_json.get("expires_in", 7200) if access_token: r.set(key, access_token, ex=int(expires)) return access_token
def refresh_access_token(self, channel_id): r = get_redis_connection() lock_name = self.token_refresh_lock % self.channel_uuid if not r.get(lock_name): with r.lock(lock_name, timeout=30): key = self.token_store_key % self.channel_uuid post_data = { "grant_type": "client_credentials", "client_id": self.app_id, "client_secret": self.app_secret, } url = self.token_url event = HttpEvent("POST", url, json.dumps(post_data)) start = time.time() response = self._request(url, post_data, access_token=None) event.status_code = response.status_code if response.status_code != 200: event.response_body = response.content ChannelLog.log_channel_request( channel_id, f"Got non-200 response from {self.api_name}", event, start, True) return response_json = response.json() event.response_body = json.dumps(response_json) ChannelLog.log_channel_request( channel_id, f"Successfully fetched access token from {self.api_name}", event, start) access_token = response_json["access_token"] expires = response_json.get("expires_in", 7200) r.set(key, access_token, ex=int(expires)) return access_token
def do_start_call(self, qs=None): client = self.channel.get_ivr_client() domain = self.channel.callback_domain from temba.ivr.clients import IVRException if client and domain: try: url = "https://%s%s" % (domain, reverse("ivr.ivrcall_handle", args=[self.pk])) if qs: # pragma: no cover url = "%s?%s" % (url, qs) tel_urn = self.contact_urn tel = tel_urn.path client.start_call(self, to=tel, from_=self.channel.address, status_callback=url) except IVRException as e: # pragma: no cover logger.error(f"Could not start IVR call: {str(e)}", exc_info=True) except Exception as e: # pragma: no cover logger.error(f"Could not start IVR call: {str(e)}", exc_info=True) ChannelLog.log_ivr_interaction( self, "Call failed unexpectedly", HttpEvent(method="INTERNAL", url=None, response_body=str(e)) ) self.status = self.FAILED self.save(update_fields=("status",)) # client or domain are not known else: ChannelLog.log_ivr_interaction( self, "Unknown client or domain", HttpEvent(method="INTERNAL", url=None, response_body=f"client={client} domain={domain}"), ) self.status = self.FAILED self.save(update_fields=("status",))
def refresh_access_token(self, channel_id): r = get_redis_connection() lock_name = self.token_refresh_lock % self.channel_uuid if not r.get(lock_name): with r.lock(lock_name, timeout=30): key = self.token_store_key % self.channel_uuid data = {"grant_type": "client_credential", "appid": self.app_id, "secret": self.app_secret} url = self.token_url event = HttpEvent("GET", url + "?" + urlencode(data)) start = time.time() response = requests.get(url, params=data, timeout=15) event.status_code = response.status_code if response.status_code != 200: event.response_body = response.content ChannelLog.log_channel_request( channel_id, f"Got non-200 response from {self.api_name}", event, start, True ) return response_json = response.json() has_error = False if response_json.get("errcode", -1) != 0: has_error = True event.response_body = json.dumps(response_json) ChannelLog.log_channel_request( channel_id, f"Successfully fetched access token from {self.api_name}", event, start, has_error ) access_token = response_json.get("access_token", "") expires = response_json.get("expires_in", 7200) if access_token: r.set(key, access_token, ex=int(expires)) return access_token
def refresh_access_token(self, channel_id): r = get_redis_connection() lock_name = self.TOKEN_REFRESH_LOCK % self.channel_uuid if not r.get(lock_name): with r.lock(lock_name, timeout=30): key = self.TOKEN_STORE_KEY % self.channel_uuid data = dict(grant_type="client_credential", appid=self.app_id, secret=self.app_secret) url = self.TOKEN_URL event = HttpEvent("GET", url + "?" + urlencode(data)) start = time.time() response = requests.get(url, params=data, timeout=15) event.status_code = response.status_code if response.status_code != 200: event.response_body = response.content ChannelLog.log_channel_request( channel_id, "Got non-200 response from %s" % self.API_NAME, event, start, True ) return response_json = response.json() has_error = False if response_json.get("errcode", -1) != 0: has_error = True event.response_body = json.dumps(response_json) ChannelLog.log_channel_request( channel_id, "Successfully fetched access token from %s" % self.API_NAME, event, start, has_error ) access_token = response_json.get("access_token", "") expires = response_json.get("expires_in", 7200) if access_token: r.set(key, access_token, ex=int(expires)) return access_token
def task_enqueue_call_events(): from .models import IVRCall r = get_redis_connection() pending_call_events = (IVRCall.objects.filter( status=IVRCall.PENDING).filter(direction=IVRCall.OUTGOING).filter( channel__is_active=True).filter( modified_on__gt=timezone.now() - timedelta(days=IVRCall.IGNORE_PENDING_CALLS_OLDER_THAN_DAYS) ).select_related("channel").order_by("modified_on")[:1000]) for call in pending_call_events: # are we handling a call on a throttled channel ? max_concurrent_events = call.channel.config.get( Channel.CONFIG_MAX_CONCURRENT_EVENTS) if max_concurrent_events: channel_key = Channel.redis_active_events_key(call.channel_id) current_active_events = r.get(channel_key) # skip this call if are on the limit if current_active_events and int( current_active_events) >= max_concurrent_events: continue else: # we can start a new call event call.register_active_event() # enqueue the call ChannelLog.log_ivr_interaction(call, "Call queued internally", HttpEvent(method="INTERNAL", url=None)) call.status = IVRCall.QUEUED call.save(update_fields=("status", )) start_call_task.apply_async(kwargs={"call_pk": call.id})
def start_call(self, call, to, from_, status_callback): if not settings.SEND_CALLS: raise IVRException("SEND_CALLS set to False, skipping call start") try: twilio_call = self.calls.create(to=to, from_=call.channel.address, url=status_callback, status_callback=status_callback) call.external_id = str(twilio_call.sid) call.save() for event in self.calls.events: ChannelLog.log_ivr_interaction(call, "Started call", event) except TwilioRestException as twilio_error: message = "Twilio Error: %s" % twilio_error.msg if twilio_error.code == 20003: message = _( "Could not authenticate with your Twilio account. Check your token and try again." ) raise IVRException(message)
def task_enqueue_call_events(): from .models import IVRCall r = get_redis_connection() pending_call_events = ( IVRCall.objects.filter(status=IVRCall.PENDING) .filter(direction=IVRCall.OUTGOING, is_active=True) .filter(channel__is_active=True) .filter(modified_on__gt=timezone.now() - timedelta(days=IVRCall.IGNORE_PENDING_CALLS_OLDER_THAN_DAYS)) .select_related("channel") .order_by("modified_on")[:1000] ) for call in pending_call_events: # are we handling a call on a throttled channel ? max_concurrent_events = call.channel.config.get(Channel.CONFIG_MAX_CONCURRENT_EVENTS) if max_concurrent_events: channel_key = Channel.redis_active_events_key(call.channel_id) current_active_events = r.get(channel_key) # skip this call if are on the limit if current_active_events and int(current_active_events) >= max_concurrent_events: continue else: # we can start a new call event call.register_active_event() # enqueue the call ChannelLog.log_ivr_interaction(call, "Call queued internally", HttpEvent(method="INTERNAL", url=None)) call.status = IVRCall.QUEUED call.save(update_fields=("status",)) start_call_task.apply_async(kwargs={"call_pk": call.id}, queue=Queue.HANDLER)
def handle_group_inbound(self, request, uuid, data): from warapidpro.types import WhatsAppGroupType channel = self.lookup_channel(WhatsAppGroupType.code, uuid) if not channel: error_msg = "Channel not found for id: %s" % (uuid, ) logger.error(error_msg) return HttpResponse(error_msg, status=400) from_addr = data['from_addr'] content = self.get_content(data) attachments = self.get_attachments(data) group_uuid = data.get('group', {}).get('uuid') # The group webhook receives messages for all groups, # only grab the message if it's a group we're a channel for. if channel.config_json()['group_uuid'] != group_uuid: logger.info('Received message for a different group.') return JsonResponse({}, status=200) message = Msg.create_incoming(channel, URN.from_tel(from_addr), content, external_id=data['uuid'], attachments=attachments) response_body = { 'message_id': message.pk, } request_body = request.body request_method = request.method request_path = request.get_full_path() event = HttpEvent(request_method, request_path, request_body, 201, json.dumps(response_body)) ChannelLog.log_message(message, 'Handled inbound message.', event) return JsonResponse(response_body, status=201)
def hangup(self, call): self.update_call(call.external_id, action="hangup", call_id=call.external_id) for event in self.events: ChannelLog.log_ivr_interaction(call, "Hung up call", event)
def hangup(self, call): response = self.calls.hangup(call.external_id) for event in self.calls.events: ChannelLog.log_ivr_interaction(call, 'Hung up call', event) return response
def post(self, request, *args, **kwargs): from temba.msgs.models import Msg request_body = request.body request_method = request.method request_path = request.get_full_path() def log_channel(channel, description, event, is_error=False): return ChannelLog.objects.create( channel_id=channel.pk, is_error=is_error, request=event.request_body, response=event.response_body, url=event.url, method=event.method, response_status=event.status_code, description=description, ) action = kwargs["action"].lower() request_uuid = kwargs["uuid"] data = json.loads(force_text(request_body)) is_ussd = self.is_ussd_message(data) channel_data = data.get("channel_data", {}) channel_types = ("JNU", "JN") # look up the channel channel = Channel.objects.filter(uuid=request_uuid, is_active=True, channel_type__in=channel_types).first() if not channel: return HttpResponse("Channel not found for id: %s" % request_uuid, status=400) auth = request.META.get("HTTP_AUTHORIZATION", "").split(" ") secret = channel.config.get(Channel.CONFIG_SECRET) if secret is not None and (len(auth) != 2 or auth[0] != "Token" or auth[1] != secret): return JsonResponse(dict(error="Incorrect authentication token"), status=401) # Junebug is sending an event if action == "event": expected_keys = ["event_type", "message_id", "timestamp"] if not set(expected_keys).issubset(data.keys()): status = 400 response_body = "Missing one of %s in request parameters." % (", ".join(expected_keys)) event = HttpEvent(request_method, request_path, request_body, status, response_body) log_channel(channel, "Failed to handle event.", event, is_error=True) return HttpResponse(response_body, status=status) message_id = data["message_id"] event_type = data["event_type"] # look up the message message = Msg.objects.filter(channel=channel, external_id=message_id).select_related("channel") if not message: status = 400 response_body = "Message with external id of '%s' not found" % (message_id,) event = HttpEvent(request_method, request_path, request_body, status, response_body) log_channel(channel, "Failed to handle %s event_type." % (event_type), event) return HttpResponse(response_body, status=status) if event_type == "submitted": for message_obj in message: message_obj.status_sent() if event_type == "delivery_succeeded": for message_obj in message: message_obj.status_delivered() elif event_type in ["delivery_failed", "rejected"]: for message_obj in message: message_obj.status_fail() response_body = {"status": self.ACK, "message_ids": [message_obj.pk for message_obj in message]} event = HttpEvent(request_method, request_path, request_body, 200, json.dumps(response_body)) log_channel(channel, "Handled %s event_type." % (event_type), event) # Let Junebug know we're happy return JsonResponse(response_body) # Handle an inbound message elif action == "inbound": expected_keys = [ "channel_data", "from", "channel_id", "timestamp", "content", "to", "reply_to", "message_id", ] if not set(expected_keys).issubset(data.keys()): status = 400 response_body = "Missing one of %s in request parameters." % (", ".join(expected_keys)) event = HttpEvent(request_method, request_path, request_body, status, response_body) log_channel(channel, "Failed to handle message.", event, is_error=True) return HttpResponse(response_body, status=status) if is_ussd: status = {"close": USSDSession.INTERRUPTED, "new": USSDSession.TRIGGERED}.get( channel_data.get("session_event"), USSDSession.IN_PROGRESS ) message_date = datetime.strptime(data["timestamp"], "%Y-%m-%d %H:%M:%S.%f") gmt_date = pytz.timezone("GMT").localize(message_date) # Use a session id if provided, otherwise fall back to using the `from` address as the identifier session_id = channel_data.get("session_id") or data["from"] connection = USSDSession.handle_incoming( channel=channel, urn=data["from"], content=data["content"], status=status, date=gmt_date, external_id=session_id, message_id=data["message_id"], starcode=data["to"], ) if connection: status = 200 response_body = {"status": self.ACK, "session_id": connection.pk} event = HttpEvent(request_method, request_path, request_body, status, json.dumps(response_body)) log_channel( channel, "Handled USSD message of %s session_event" % (channel_data["session_event"],), event ) return JsonResponse(response_body, status=status) else: status = 400 response_body = {"status": self.NACK, "reason": "No suitable session found for this message."} event = HttpEvent(request_method, request_path, request_body, status, json.dumps(response_body)) log_channel( channel, "Failed to handle USSD message of %s session_event" % (channel_data["session_event"],), event, ) return JsonResponse(response_body, status=status) else: content = data["content"] message = Msg.create_incoming(channel, URN.from_tel(data["from"]), content) status = 200 response_body = {"status": self.ACK, "message_id": message.pk} Msg.objects.filter(pk=message.id).update(external_id=data["message_id"]) event = HttpEvent(request_method, request_path, request_body, status, json.dumps(response_body)) ChannelLog.log_message(message, "Handled inbound message.", event) return JsonResponse(response_body, status=status)
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 do_start_call(self, qs=None): client = self.channel.get_ivr_client() domain = self.channel.callback_domain from temba.ivr.clients import IVRException from temba.flows.models import ActionLog, FlowRun if client and domain: try: url = "https://%s%s" % (domain, reverse("ivr.ivrcall_handle", args=[self.pk])) if qs: # pragma: no cover url = "%s?%s" % (url, qs) tel = None # if we are working with a test contact, look for user settings if self.contact.is_test: user_settings = self.created_by.get_settings() if user_settings.tel: tel = user_settings.tel run = FlowRun.objects.filter(connection=self) if run: ActionLog.create(run[0], "Placing test call to %s" % user_settings.get_tel_formatted()) if not tel: tel_urn = self.contact_urn tel = tel_urn.path client.start_call(self, to=tel, from_=self.channel.address, status_callback=url) except IVRException as e: import traceback traceback.print_exc() if self.contact.is_test: run = FlowRun.objects.filter(connection=self) ActionLog.create(run[0], "Call ended. %s" % str(e)) except Exception as e: # pragma: no cover import traceback traceback.print_exc() ChannelLog.log_ivr_interaction( self, "Call failed unexpectedly", HttpEvent(method="INTERNAL", url=None, response_body=str(e)) ) self.status = self.FAILED self.save(update_fields=("status",)) if self.contact.is_test: run = FlowRun.objects.filter(connection=self) ActionLog.create(run[0], "Call ended.") # client or domain are not known else: ChannelLog.log_ivr_interaction( self, "Unknown client or domain", HttpEvent(method="INTERNAL", url=None, response_body=f"client={client} domain={domain}"), ) self.status = self.FAILED self.save(update_fields=("status",))
def post(self, request, *args, **kwargs): from temba.msgs.models import Msg request_body = request.body request_method = request.method request_path = request.get_full_path() def log_channel(channel, description, event, is_error=False): return ChannelLog.objects.create( channel_id=channel.pk, is_error=is_error, request=event.request_body, response=event.response_body, url=event.url, method=event.method, response_status=event.status_code, description=description, ) action = kwargs["action"].lower() request_uuid = kwargs["uuid"] data = json.loads(force_text(request_body)) is_ussd = self.is_ussd_message(data) channel_data = data.get("channel_data", {}) channel_types = ("JNU", "JN") # look up the channel channel = Channel.objects.filter( uuid=request_uuid, is_active=True, channel_type__in=channel_types).first() if not channel: return HttpResponse("Channel not found for id: %s" % request_uuid, status=400) auth = request.META.get("HTTP_AUTHORIZATION", "").split(" ") secret = channel.config.get(Channel.CONFIG_SECRET) if secret is not None and (len(auth) != 2 or auth[0] != "Token" or auth[1] != secret): return JsonResponse(dict(error="Incorrect authentication token"), status=401) # Junebug is sending an event if action == "event": expected_keys = ["event_type", "message_id", "timestamp"] if not set(expected_keys).issubset(data.keys()): status = 400 response_body = "Missing one of %s in request parameters." % ( ", ".join(expected_keys)) event = HttpEvent(request_method, request_path, request_body, status, response_body) log_channel(channel, "Failed to handle event.", event, is_error=True) return HttpResponse(response_body, status=status) message_id = data["message_id"] event_type = data["event_type"] # look up the message message = Msg.objects.filter( channel=channel, external_id=message_id).select_related("channel") if not message: status = 400 response_body = "Message with external id of '%s' not found" % ( message_id, ) event = HttpEvent(request_method, request_path, request_body, status, response_body) log_channel(channel, "Failed to handle %s event_type." % (event_type), event) return HttpResponse(response_body, status=status) if event_type == "submitted": for message_obj in message: message_obj.status_sent() if event_type == "delivery_succeeded": for message_obj in message: message_obj.status_delivered() elif event_type in ["delivery_failed", "rejected"]: for message_obj in message: message_obj.status_fail() response_body = { "status": self.ACK, "message_ids": [message_obj.pk for message_obj in message] } event = HttpEvent(request_method, request_path, request_body, 200, json.dumps(response_body)) log_channel(channel, "Handled %s event_type." % (event_type), event) # Let Junebug know we're happy return JsonResponse(response_body) # Handle an inbound message elif action == "inbound": expected_keys = [ "channel_data", "from", "channel_id", "timestamp", "content", "to", "reply_to", "message_id", ] if not set(expected_keys).issubset(data.keys()): status = 400 response_body = "Missing one of %s in request parameters." % ( ", ".join(expected_keys)) event = HttpEvent(request_method, request_path, request_body, status, response_body) log_channel(channel, "Failed to handle message.", event, is_error=True) return HttpResponse(response_body, status=status) if is_ussd: status = { "close": USSDSession.INTERRUPTED, "new": USSDSession.TRIGGERED }.get(channel_data.get("session_event"), USSDSession.IN_PROGRESS) message_date = datetime.strptime(data["timestamp"], "%Y-%m-%d %H:%M:%S.%f") gmt_date = pytz.timezone("GMT").localize(message_date) # Use a session id if provided, otherwise fall back to using the `from` address as the identifier session_id = channel_data.get("session_id") or data["from"] connection = USSDSession.handle_incoming( channel=channel, urn=data["from"], content=data["content"], status=status, date=gmt_date, external_id=session_id, message_id=data["message_id"], starcode=data["to"], ) if connection: status = 200 response_body = { "status": self.ACK, "session_id": connection.pk } event = HttpEvent(request_method, request_path, request_body, status, json.dumps(response_body)) log_channel( channel, "Handled USSD message of %s session_event" % (channel_data["session_event"], ), event) return JsonResponse(response_body, status=status) else: status = 400 response_body = { "status": self.NACK, "reason": "No suitable session found for this message." } event = HttpEvent(request_method, request_path, request_body, status, json.dumps(response_body)) log_channel( channel, "Failed to handle USSD message of %s session_event" % (channel_data["session_event"], ), event, ) return JsonResponse(response_body, status=status) else: content = data["content"] message = Msg.create_incoming(channel, URN.from_tel(data["from"]), content) status = 200 response_body = {"status": self.ACK, "message_id": message.pk} Msg.objects.filter(pk=message.id).update( external_id=data["message_id"]) event = HttpEvent(request_method, request_path, request_body, status, json.dumps(response_body)) ChannelLog.log_message(message, "Handled inbound message.", event) return JsonResponse(response_body, status=status)
def hangup(self, call): twilio_call = self.api.calls.get(call.external_id).update(status="completed") for event in self.events: ChannelLog.log_ivr_interaction(call, "Hung up call", event) return twilio_call
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 post(self, request, *args, **kwargs): call = IVRCall.objects.filter(pk=kwargs["pk"]).first() if not call: return HttpResponse("Not found", status=404) channel = call.channel if not (channel.is_active and channel.org): return HttpResponse("No channel found", status=400) channel_type = channel.channel_type ivr_protocol = Channel.get_type_from_code(channel_type).ivr_protocol client = channel.get_ivr_client() request_body = force_text(request.body) request_method = request.method request_path = request.get_full_path() if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_TWIML and request.POST.get( "hangup", 0): if not request.user.is_anonymous: user_org = request.user.get_org() if user_org and user_org.pk == call.org.pk: client.hangup(call) return HttpResponse(json.dumps(dict(status="Canceled")), content_type="application/json") else: # pragma: no cover return HttpResponse("Not found", status=404) input_redirect = "1" == request.GET.get("input_redirect", "0") if client.validate(request): status = None duration = None if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_TWIML: status = request.POST.get("CallStatus", None) duration = request.POST.get("CallDuration", None) elif ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_NCCO: if request_body: body_json = json.loads(request_body) status = body_json.get("status", None) duration = body_json.get("duration", None) # force in progress call status for fake (input) redirects if input_redirect: status = "answered" # nexmo does not set status for some callbacks if status is not None: call.update_status( status, duration, channel_type ) # update any calls we have spawned with the same call.save() resume = request.GET.get("resume", 0) user_response = request.POST.copy() hangup = False saved_media_url = None text = None media_url = None has_event = False if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_TWIML: # figure out if this is a callback due to an empty gather is_empty = "1" == request.GET.get("empty", "0") # if the user pressed pound, then record no digits as the input if is_empty: user_response["Digits"] = "" hangup = "hangup" == user_response.get("Digits", None) media_url = user_response.get("RecordingUrl", None) # if we've been sent a recording, go grab it if media_url: saved_media_url = client.download_media(media_url) # parse the user response text = user_response.get("Digits", None) elif ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_NCCO: if request_body: body_json = json.loads(request_body) media_url = body_json.get("recording_url", None) if media_url: cache.set("last_call:media_url:%d" % call.pk, media_url, None) media_url = cache.get("last_call:media_url:%d" % call.pk, None) text = body_json.get("dtmf", None) if input_redirect: text = None has_event = "1" == request.GET.get("has_event", "0") save_media = "1" == request.GET.get("save_media", "0") if media_url: if save_media: saved_media_url = client.download_media( call, media_url) cache.delete("last_call:media_url:%d" % call.pk) else: response_msg = "Saved media url" response = dict(message=response_msg) event = HttpEvent(request_method, request_path, request_body, 200, json.dumps(response)) ChannelLog.log_ivr_interaction(call, response_msg, event) return JsonResponse(response) if not has_event and call.status not in IVRCall.DONE or hangup: if call.is_ivr(): response = Flow.handle_call( call, text=text, saved_media_url=saved_media_url, hangup=hangup, resume=resume) event = HttpEvent(request_method, request_path, request_body, 200, str(response)) if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_NCCO: ChannelLog.log_ivr_interaction( call, "Incoming request for call", event) # TODO: what's special here that this needs to be different? return JsonResponse(json.loads(str(response)), safe=False) ChannelLog.log_ivr_interaction( call, "Incoming request for call", event) return HttpResponse(str(response), content_type="text/xml; charset=utf-8") else: if call.status == IVRCall.COMPLETED: # if our call is completed, hangup runs = FlowRun.objects.filter(connection=call) for run in runs: if not run.is_completed(): run.set_completed(exit_uuid=None) response = dict( description="Updated call status", call=dict(status=call.get_status_display(), duration=call.duration), ) event = HttpEvent(request_method, request_path, request_body, 200, json.dumps(response)) ChannelLog.log_ivr_interaction(call, "Updated call status", event) return JsonResponse(response) else: # pragma: no cover error = "Invalid request signature" event = HttpEvent(request_method, request_path, request_body, 200, error) ChannelLog.log_ivr_interaction(call, error, event, is_error=True) # raise an exception that things weren't properly signed raise ValidationError(error) return JsonResponse(dict(message="Unhandled")) # pragma: no cover
def post(self, request, *args, **kwargs): call = IVRCall.objects.filter(pk=kwargs["pk"]).first() if not call: return HttpResponse("Not found", status=404) channel = call.channel if not (channel.is_active and channel.org): return HttpResponse("No channel found", status=400) channel_type = channel.channel_type ivr_protocol = Channel.get_type_from_code(channel_type).ivr_protocol client = channel.get_ivr_client() request_body = force_text(request.body) request_method = request.method request_path = request.get_full_path() if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_TWIML and request.POST.get("hangup", 0): if not request.user.is_anonymous: user_org = request.user.get_org() if user_org and user_org.pk == call.org.pk: client.hangup(call) return HttpResponse(json.dumps(dict(status="Canceled")), content_type="application/json") else: # pragma: no cover return HttpResponse("Not found", status=404) input_redirect = "1" == request.GET.get("input_redirect", "0") if client.validate(request): status = None duration = None if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_TWIML: status = request.POST.get("CallStatus", None) duration = request.POST.get("CallDuration", None) elif ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_NCCO: if request_body: body_json = json.loads(request_body) status = body_json.get("status", None) duration = body_json.get("duration", None) # force in progress call status for fake (input) redirects if input_redirect: status = "answered" # nexmo does not set status for some callbacks if status is not None: call.update_status(status, duration, channel_type) # update any calls we have spawned with the same call.save() resume = request.GET.get("resume", 0) user_response = request.POST.copy() hangup = False saved_media_url = None text = None media_url = None has_event = False if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_TWIML: # figure out if this is a callback due to an empty gather is_empty = "1" == request.GET.get("empty", "0") # if the user pressed pound, then record no digits as the input if is_empty: user_response["Digits"] = "" hangup = "hangup" == user_response.get("Digits", None) media_url = user_response.get("RecordingUrl", None) # if we've been sent a recording, go grab it if media_url: saved_media_url = client.download_media(media_url) # parse the user response text = user_response.get("Digits", None) elif ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_NCCO: if request_body: body_json = json.loads(request_body) media_url = body_json.get("recording_url", None) if media_url: cache.set("last_call:media_url:%d" % call.pk, media_url, None) media_url = cache.get("last_call:media_url:%d" % call.pk, None) text = body_json.get("dtmf", None) if input_redirect: text = None has_event = "1" == request.GET.get("has_event", "0") save_media = "1" == request.GET.get("save_media", "0") if media_url: if save_media: saved_media_url = client.download_media(call, media_url) cache.delete("last_call:media_url:%d" % call.pk) else: response_msg = "Saved media url" response = dict(message=response_msg) event = HttpEvent(request_method, request_path, request_body, 200, json.dumps(response)) ChannelLog.log_ivr_interaction(call, response_msg, event) return JsonResponse(response) if not has_event and call.status not in IVRCall.DONE or hangup: if call.is_ivr(): response = Flow.handle_call( call, text=text, saved_media_url=saved_media_url, hangup=hangup, resume=resume ) event = HttpEvent(request_method, request_path, request_body, 200, str(response)) if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_NCCO: ChannelLog.log_ivr_interaction(call, "Incoming request for call", event) # TODO: what's special here that this needs to be different? return JsonResponse(json.loads(str(response)), safe=False) ChannelLog.log_ivr_interaction(call, "Incoming request for call", event) return HttpResponse(str(response), content_type="text/xml; charset=utf-8") else: if call.status == IVRCall.COMPLETED: # if our call is completed, hangup runs = FlowRun.objects.filter(connection=call) for run in runs: if not run.is_completed(): run.set_completed(exit_uuid=None) response = dict( description="Updated call status", call=dict(status=call.get_status_display(), duration=call.duration), ) event = HttpEvent(request_method, request_path, request_body, 200, json.dumps(response)) ChannelLog.log_ivr_interaction(call, "Updated call status", event) return JsonResponse(response) else: # pragma: no cover error = "Invalid request signature" event = HttpEvent(request_method, request_path, request_body, 200, error) ChannelLog.log_ivr_interaction(call, error, event, is_error=True) # raise an exception that things weren't properly signed raise ValidationError(error) return JsonResponse(dict(message="Unhandled")) # pragma: no cover
def post(self, request, *args, **kwargs): from temba.msgs.models import Msg request_body = request.body request_method = request.method request_path = request.get_full_path() def log_channel(channel, description, event, is_error=False): return ChannelLog.objects.create(channel_id=channel.pk, is_error=is_error, request=event.request_body, response=event.response_body, url=event.url, method=event.method, response_status=event.status_code, description=description) action = kwargs['action'].lower() request_uuid = kwargs['uuid'] data = json.loads(force_text(request_body)) is_ussd = self.is_ussd_message(data) channel_data = data.get('channel_data', {}) channel_types = ('JNU', 'JN') # look up the channel channel = Channel.objects.filter( uuid=request_uuid, is_active=True, channel_type__in=channel_types).first() if not channel: return HttpResponse("Channel not found for id: %s" % request_uuid, status=400) auth = request.META.get('HTTP_AUTHORIZATION', '').split(' ') secret = channel.config.get(Channel.CONFIG_SECRET) if secret is not None and (len(auth) != 2 or auth[0] != 'Token' or auth[1] != secret): return JsonResponse(dict(error="Incorrect authentication token"), status=401) # Junebug is sending an event if action == 'event': expected_keys = ["event_type", "message_id", "timestamp"] if not set(expected_keys).issubset(data.keys()): status = 400 response_body = "Missing one of %s in request parameters." % ( ', '.join(expected_keys)) event = HttpEvent(request_method, request_path, request_body, status, response_body) log_channel(channel, 'Failed to handle event.', event, is_error=True) return HttpResponse(response_body, status=status) message_id = data['message_id'] event_type = data["event_type"] # look up the message message = Msg.objects.filter( channel=channel, external_id=message_id).select_related('channel') if not message: status = 400 response_body = "Message with external id of '%s' not found" % ( message_id, ) event = HttpEvent(request_method, request_path, request_body, status, response_body) log_channel(channel, 'Failed to handle %s event_type.' % (event_type), event) return HttpResponse(response_body, status=status) if event_type == 'submitted': for message_obj in message: message_obj.status_sent() if event_type == 'delivery_succeeded': for message_obj in message: message_obj.status_delivered() elif event_type in ['delivery_failed', 'rejected']: for message_obj in message: message_obj.status_fail() response_body = { 'status': self.ACK, 'message_ids': [message_obj.pk for message_obj in message] } event = HttpEvent(request_method, request_path, request_body, 200, json.dumps(response_body)) log_channel(channel, 'Handled %s event_type.' % (event_type), event) # Let Junebug know we're happy return JsonResponse(response_body) # Handle an inbound message elif action == 'inbound': expected_keys = [ 'channel_data', 'from', 'channel_id', 'timestamp', 'content', 'to', 'reply_to', 'message_id', ] if not set(expected_keys).issubset(data.keys()): status = 400 response_body = "Missing one of %s in request parameters." % ( ', '.join(expected_keys)) event = HttpEvent(request_method, request_path, request_body, status, response_body) log_channel(channel, 'Failed to handle message.', event, is_error=True) return HttpResponse(response_body, status=status) if is_ussd: status = { 'close': USSDSession.INTERRUPTED, 'new': USSDSession.TRIGGERED, }.get(channel_data.get('session_event'), USSDSession.IN_PROGRESS) message_date = datetime.strptime(data['timestamp'], "%Y-%m-%d %H:%M:%S.%f") gmt_date = pytz.timezone('GMT').localize(message_date) # Use a session id if provided, otherwise fall back to using the `from` address as the identifier session_id = channel_data.get('session_id') or data['from'] connection = USSDSession.handle_incoming( channel=channel, urn=data['from'], content=data['content'], status=status, date=gmt_date, external_id=session_id, message_id=data['message_id'], starcode=data['to']) if connection: status = 200 response_body = { 'status': self.ACK, 'session_id': connection.pk, } event = HttpEvent(request_method, request_path, request_body, status, json.dumps(response_body)) log_channel( channel, 'Handled USSD message of %s session_event' % (channel_data['session_event'], ), event) return JsonResponse(response_body, status=status) else: status = 400 response_body = { 'status': self.NACK, 'reason': 'No suitable session found for this message.' } event = HttpEvent(request_method, request_path, request_body, status, json.dumps(response_body)) log_channel( channel, 'Failed to handle USSD message of %s session_event' % (channel_data['session_event'], ), event) return JsonResponse(response_body, status=status) else: content = data['content'] message = Msg.create_incoming(channel, URN.from_tel(data['from']), content) status = 200 response_body = { 'status': self.ACK, 'message_id': message.pk, } Msg.objects.filter(pk=message.id).update( external_id=data['message_id']) event = HttpEvent(request_method, request_path, request_body, status, json.dumps(response_body)) ChannelLog.log_message(message, 'Handled inbound message.', event) return JsonResponse(response_body, status=status)
def post(self, request, *args, **kwargs): call = IVRCall.objects.filter(pk=kwargs['pk']).first() if not call: return HttpResponse("Not found", status=404) channel = call.channel channel_type = channel.channel_type client = channel.get_ivr_client() request_body = request.body request_method = request.method request_path = request.get_full_path() if channel_type in Channel.TWIML_CHANNELS and request.POST.get( 'hangup', 0): if not request.user.is_anonymous(): user_org = request.user.get_org() if user_org and user_org.pk == call.org.pk: client.calls.hangup(call.external_id) return HttpResponse(json.dumps(dict(status='Canceled')), content_type="application/json") else: # pragma: no cover return HttpResponse("Not found", status=404) input_redirect = '1' == request.GET.get('input_redirect', '0') if client.validate(request): status = None duration = None if channel_type in Channel.TWIML_CHANNELS: status = request.POST.get('CallStatus', None) duration = request.POST.get('CallDuration', None) elif channel_type in Channel.NCCO_CHANNELS: if request_body: body_json = json.loads(request_body) status = body_json.get('status', None) duration = body_json.get('duration', None) # force in progress call status for fake (input) redirects if input_redirect: status = 'answered' call.update_status( status, duration, channel_type) # update any calls we have spawned with the same call.save() resume = request.GET.get('resume', 0) user_response = request.POST.copy() hangup = False saved_media_url = None text = None media_url = None if channel_type in Channel.TWIML_CHANNELS: # figure out if this is a callback due to an empty gather is_empty = '1' == request.GET.get('empty', '0') # if the user pressed pound, then record no digits as the input if is_empty: user_response['Digits'] = '' hangup = 'hangup' == user_response.get('Digits', None) media_url = user_response.get('RecordingUrl', None) # if we've been sent a recording, go grab it if media_url: saved_media_url = client.download_media(media_url) # parse the user response text = user_response.get('Digits', None) elif channel_type in Channel.NCCO_CHANNELS: if request_body: body_json = json.loads(request_body) media_url = body_json.get('recording_url', None) if media_url: cache.set('last_call:media_url:%d' % call.pk, media_url, None) media_url = cache.get('last_call:media_url:%d' % call.pk, None) text = body_json.get('dtmf', None) if input_redirect: text = None has_event = '1' == request.GET.get('has_event', '0') if has_event: return HttpResponse(six.text_type('')) save_media = '1' == request.GET.get('save_media', '0') if media_url: if save_media: saved_media_url = client.download_media(media_url) cache.delete('last_call:media_url:%d' % call.pk) else: response_msg = 'media URL saved' ChannelLog.log_ivr_interaction( call, "Saved media URL", request_body, six.text_type(response_msg), request_path, request_method) return HttpResponse(six.text_type(response_msg)) if call.status not in IVRCall.DONE or hangup: if call.is_ivr(): response = Flow.handle_call( call, text=text, saved_media_url=saved_media_url, hangup=hangup, resume=resume) if channel_type in Channel.NCCO_CHANNELS: ChannelLog.log_ivr_interaction(call, "Returned response", request_body, six.text_type(response), request_path, request_method) return JsonResponse(json.loads( six.text_type(response)), safe=False) ChannelLog.log_ivr_interaction(call, "Returned response", request_body, six.text_type(response), request_path, request_method) return HttpResponse(six.text_type(response)) else: if call.status == IVRCall.COMPLETED: # if our call is completed, hangup run = FlowRun.objects.filter(session=call).first() if run: run.set_completed() response = dict(message="Updated call status", call=dict(status=call.get_status_display(), duration=call.duration)) ChannelLog.log_ivr_interaction( call, "Updated call status: %s" % call.get_status_display(), request_body, json.dumps(response), request_path, request_method) return JsonResponse(response) else: # pragma: no cover error_message = "Invalid request signature" ChannelLog.log_ivr_interaction(call, error_message, request_body, error_message, request_path, request_method, is_error=True) # raise an exception that things weren't properly signed raise ValidationError(error_message) return JsonResponse(dict(message="Unhandled")) # pragma: no cover