def update_status(self, status: str, duration: float, channel_type: str): """ Updates our status from a provide call status string """ if not status: raise ValueError(f"IVR Call status must be defined, got: '{status}'") previous_status = self.status from temba.flows.models import FlowRun ivr_protocol = Channel.get_type_from_code(channel_type).ivr_protocol if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_TWIML: self.status = self.derive_ivr_status_twiml(status, previous_status) elif ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_NCCO: self.status = self.derive_ivr_status_nexmo(status, previous_status) else: # pragma: no cover raise ValueError(f"Unhandled IVR protocol: {ivr_protocol}") # if we are in progress, mark our start time if self.status == self.IN_PROGRESS and previous_status != self.IN_PROGRESS: self.started_on = timezone.now() # if we are done, mark our ended time if self.status in ChannelConnection.DONE: self.ended_on = timezone.now() self.unregister_active_event() from temba.flows.models import FlowSession if self.has_flow_session(): self.session.end(FlowSession.STATUS_COMPLETED) if self.status in ChannelConnection.RETRY_CALL and previous_status not in ChannelConnection.RETRY_CALL: flow = self.get_flow() backoff_minutes = flow.metadata.get("ivr_retry", IVRCall.RETRY_BACKOFF_MINUTES) self.schedule_call_retry(backoff_minutes) if duration is not None: self.duration = duration # if we are moving into IN_PROGRESS, make sure our runs have proper expirations if previous_status in (self.PENDING, self.QUEUED, self.WIRED) and self.status in ( self.IN_PROGRESS, self.RINGING, ): runs = FlowRun.objects.filter(connection=self, is_active=True) for run in runs: if not run.expires_on or ( run.expires_on - run.modified_on > timedelta(minutes=self.IVR_EXPIRES_CHOICES[-1][0]) ): run.update_expiration() if self.status == ChannelConnection.FAILED: flow = self.get_flow() if flow.metadata.get("ivr_retry_failed_events"): self.schedule_failed_call_retry()
def update_status(self, status, duration, channel_type): """ Updates our status from a provide call status string """ from temba.flows.models import FlowRun, ActionLog previous_status = self.status ivr_protocol = Channel.get_type_from_code(channel_type).ivr_protocol if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_TWIML: if status == 'queued': self.status = self.QUEUED elif status == 'ringing': self.status = self.RINGING elif status == 'no-answer': self.status = self.NO_ANSWER elif status == 'in-progress': if self.status != self.IN_PROGRESS: self.started_on = timezone.now() self.status = self.IN_PROGRESS elif status == 'completed': if self.contact.is_test: run = FlowRun.objects.filter(connection=self) if run: ActionLog.create(run[0], _("Call ended.")) self.status = self.COMPLETED elif status == 'busy': self.status = self.BUSY elif status == 'failed': self.status = self.FAILED elif status == 'canceled': self.status = self.CANCELED elif ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_NCCO: if status in ('ringing', 'started'): self.status = self.RINGING elif status == 'answered': self.status = self.IN_PROGRESS elif status == 'completed': self.status = self.COMPLETED elif status == 'failed': self.status = self.FAILED elif status in ('rejected', 'busy'): self.status = self.BUSY elif status in ('unanswered', 'timeout'): self.status = self.NO_ANSWER # if we are done, mark our ended time if self.status in ChannelSession.DONE: self.ended_on = timezone.now() if duration is not None: self.duration = duration # if we are moving into IN_PROGRESS, make sure our runs have proper expirations if previous_status in [self.QUEUED, self.PENDING] and self.status in [self.IN_PROGRESS, self.RINGING]: runs = FlowRun.objects.filter(connection=self, is_active=True, expires_on=None) for run in runs: run.update_expiration()
def create_channel( self, channel_type: str, name: str, address: str, role=None, schemes=None, country=None, secret=None, config=None, org=None, ): channel_type = Channel.get_type_from_code(channel_type) return Channel.objects.create( org=org or self.org, country=country, channel_type=channel_type.code, name=name, address=address, config=config or {}, role=role or Channel.DEFAULT_ROLE, secret=secret, schemes=schemes or channel_type.schemes, created_by=self.admin, modified_by=self.admin, )
def update_status(self, status: str, duration: float, channel_type: str): """ Updates our status from a provide call status string """ if not status: raise ValueError( f"IVR Call status must be defined, got: '{status}'") previous_status = self.status from temba.flows.models import FlowRun, ActionLog ivr_protocol = Channel.get_type_from_code(channel_type).ivr_protocol if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_TWIML: self.status = self.derive_ivr_status_twiml(status, previous_status) elif ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_NCCO: self.status = self.derive_ivr_status_nexmo(status, previous_status) else: # pragma: no cover raise ValueError(f"Unhandled IVR protocol: {ivr_protocol}") # if we are in progress, mark our start time if self.status == self.IN_PROGRESS and previous_status != self.IN_PROGRESS: self.started_on = timezone.now() # if we are done, mark our ended time if self.status in ChannelSession.DONE: self.ended_on = timezone.now() if self.contact.is_test: run = FlowRun.objects.filter(connection=self) if run: ActionLog.create(run[0], _("Call ended.")) if self.status in ChannelSession.RETRY_CALL and previous_status not in ChannelSession.RETRY_CALL: flow = self.get_flow() backoff_minutes = flow.metadata.get("ivr_retry", IVRCall.RETRY_BACKOFF_MINUTES) self.schedule_call_retry(backoff_minutes) if duration is not None: self.duration = duration # if we are moving into IN_PROGRESS, make sure our runs have proper expirations if previous_status in (self.PENDING, self.QUEUED, self.WIRED) and self.status in ( self.IN_PROGRESS, self.RINGING, ): runs = FlowRun.objects.filter(connection=self, is_active=True, expires_on=None) for run in runs: run.update_expiration()
def handle_simulate(self, num_runs, org_id, flow_name, seed): """ Prepares to resume simulating flow activity on an existing database """ self._log( "Resuming flow activity simulation on existing database...\n") orgs = Org.objects.order_by('id') if org_id: orgs = orgs.filter(id=org_id) if not orgs: raise CommandError("Can't simulate activity on an empty database") self.configure_random(len(orgs), seed) # in real life Nexmo messages are throttled, but that's not necessary for this simulation Channel.get_type_from_code('NX').max_tps = None inputs_by_flow_name = {f['name']: f['templates'] for f in FLOWS} self._log("Preparing existing orgs... ") for org in orgs: flows = org.flows.order_by('id') if flow_name: flows = flows.filter(name=flow_name) flows = list(flows) for flow in flows: flow.input_templates = inputs_by_flow_name[flow.name] org.cache = { 'users': list(org.get_org_users().order_by('id')), 'channels': list(org.channels.order_by('id')), 'groups': list(ContactGroup.user_groups.filter(org=org).order_by('id')), 'flows': flows, 'contacts': list(org.org_contacts.values_list( 'id', flat=True)), # only ids to save memory 'activity': None } self._log(self.style.SUCCESS("OK") + '\n') self.simulate_activity(orgs, num_runs)
def update_status(self, status: str, duration: float, channel_type: str): """ Updates our status from a provide call status string """ if not status: raise ValueError(f"IVR Call status must be defined, got: '{status}'") previous_status = self.status from temba.flows.models import FlowRun, ActionLog ivr_protocol = Channel.get_type_from_code(channel_type).ivr_protocol if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_TWIML: self.status = self.derive_ivr_status_twiml(status, previous_status) elif ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_NCCO: self.status = self.derive_ivr_status_nexmo(status, previous_status) else: # pragma: no cover raise ValueError(f"Unhandled IVR protocol: {ivr_protocol}") # if we are in progress, mark our start time if self.status == self.IN_PROGRESS and previous_status != self.IN_PROGRESS: self.started_on = timezone.now() # if we are done, mark our ended time if self.status in ChannelSession.DONE: self.ended_on = timezone.now() if self.contact.is_test: run = FlowRun.objects.filter(connection=self) if run: ActionLog.create(run[0], _("Call ended.")) if self.status in ChannelSession.RETRY_CALL and previous_status not in ChannelSession.RETRY_CALL: flow = self.get_flow() backoff_minutes = flow.metadata.get("ivr_retry", IVRCall.RETRY_BACKOFF_MINUTES) self.schedule_call_retry(backoff_minutes) if duration is not None: self.duration = duration # if we are moving into IN_PROGRESS, make sure our runs have proper expirations if previous_status in (self.PENDING, self.QUEUED, self.WIRED) and self.status in ( self.IN_PROGRESS, self.RINGING, ): runs = FlowRun.objects.filter(connection=self, is_active=True, expires_on=None) for run in runs: run.update_expiration()
def handle_simulate(self, num_runs, org_id, flow_name, seed): """ Prepares to resume simulating flow activity on an existing database """ self._log("Resuming flow activity simulation on existing database...\n") orgs = Org.objects.order_by("id") if org_id: orgs = orgs.filter(id=org_id) if not orgs: raise CommandError("Can't simulate activity on an empty database") self.configure_random(len(orgs), seed) # in real life Nexmo messages are throttled, but that's not necessary for this simulation Channel.get_type_from_code("NX").max_tps = None inputs_by_flow_name = {f["name"]: f["templates"] for f in FLOWS} self._log("Preparing existing orgs... ") for org in orgs: flows = org.flows.order_by("id").exclude(is_system=True) if flow_name: flows = flows.filter(name=flow_name) flows = list(flows) for flow in flows: flow.input_templates = inputs_by_flow_name[flow.name] org.cache = { "users": list(org.get_org_users().order_by("id")), "channels": list(org.channels.order_by("id")), "groups": list(ContactGroup.user_groups.filter(org=org).order_by("id")), "flows": flows, "contacts": list(org.org_contacts.values_list("id", flat=True)), # only ids to save memory "activity": None, } self._log(self.style.SUCCESS("OK") + "\n") self.simulate_activity(orgs, num_runs)
def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) is_support = self.request.user.groups.filter( name="Customer Support").first() end = timezone.now() begin = end - timedelta(days=30) begin = self.request.GET.get("begin", datetime.strftime(begin, "%Y-%m-%d")) end = self.request.GET.get("end", datetime.strftime(end, "%Y-%m-%d")) direction = self.request.GET.get("direction", "IO") if begin and end: orgs = [] org = self.derive_org() if org: orgs = Org.objects.filter(Q(id=org.id) | Q(parent=org)) count_types = [] if "O" in direction: count_types = [ ChannelCount.OUTGOING_MSG_TYPE, ChannelCount.OUTGOING_IVR_TYPE ] if "I" in direction: count_types += [ ChannelCount.INCOMING_MSG_TYPE, ChannelCount.INCOMING_IVR_TYPE ] # get all our counts for that period daily_counts = (ChannelCount.objects.filter( count_type__in=count_types).filter(day__gte=begin).filter( day__lte=end).exclude(channel__org=None)) if orgs: daily_counts = daily_counts.filter(channel__org__in=orgs) context["orgs"] = list( daily_counts.values( "channel__org", "channel__org__name").order_by("-count_sum").annotate( count_sum=Sum("count"))[:12]) channel_types = (ChannelCount.objects.filter( count_type__in=count_types).filter(day__gte=begin).filter( day__lte=end).exclude(channel__org=None)) if orgs or not is_support: channel_types = channel_types.filter(channel__org__in=orgs) channel_types = list( channel_types.values("channel__channel_type").order_by( "-count_sum").annotate(count_sum=Sum("count"))) # populate the channel names pie = [] for channel_type in channel_types[0:6]: channel_type["channel__name"] = Channel.get_type_from_code( channel_type["channel__channel_type"]).name pie.append(channel_type) other_count = 0 for channel_type in channel_types[6:]: other_count += channel_type["count_sum"] if other_count: pie.append(dict(channel__name="Other", count_sum=other_count)) context["channel_types"] = pie context["begin"] = datetime.strptime(begin, "%Y-%m-%d").date() context["end"] = datetime.strptime(end, "%Y-%m-%d").date() context["direction"] = direction return context
def 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 send(self, channel, msg, text): connection = None # if the channel config has specified and override hostname use that, otherwise use settings event_hostname = channel.config.get(Channel.CONFIG_RP_HOSTNAME_OVERRIDE, settings.HOSTNAME) # the event url Junebug will relay events to event_url = 'http://%s%s' % (event_hostname, reverse('courier.jn', args=[channel.uuid, 'event'])) is_ussd = Channel.get_type_from_code(channel.channel_type).category == ChannelType.Category.USSD # build our payload payload = {'event_url': event_url, 'content': text} if channel.secret is not None: payload['event_auth_token'] = channel.secret if is_ussd: connection = USSDSession.objects.get_with_status_only(msg.connection_id) # make sure USSD responses are only valid for a short window response_expiration = timezone.now() - timedelta(seconds=180) external_id = None if msg.response_to_id and msg.created_on > response_expiration: external_id = Msg.objects.values_list('external_id', flat=True).filter(pk=msg.response_to_id).first() # NOTE: Only one of `to` or `reply_to` may be specified, use external_id if we have it. if external_id: payload['reply_to'] = external_id else: payload['to'] = msg.urn_path payload['channel_data'] = { 'continue_session': connection and not connection.should_end or False, } else: payload['from'] = channel.address payload['to'] = msg.urn_path log_url = channel.config[Channel.CONFIG_SEND_URL] start = time.time() event = HttpEvent('POST', log_url, json.dumps(payload)) headers = {'Content-Type': 'application/json'} headers.update(TEMBA_HEADERS) try: response = requests.post( channel.config[Channel.CONFIG_SEND_URL], verify=True, json=payload, timeout=15, headers=headers, auth=(channel.config[Channel.CONFIG_USERNAME], channel.config[Channel.CONFIG_PASSWORD])) event.status_code = response.status_code event.response_body = response.text except Exception as e: raise SendException(unicode(e), event=event, start=start) if not (200 <= response.status_code < 300): raise SendException("Received a non 200 response %d from Junebug" % response.status_code, event=event, start=start) data = response.json() if is_ussd and connection and connection.should_end: connection.close() try: message_id = data['result']['message_id'] Channel.success(channel, msg, WIRED, start, event=event, external_id=message_id) except KeyError, e: raise SendException("Unable to read external message_id: %r" % (e,), event=HttpEvent('POST', log_url, request_body=json.dumps(json.dumps(payload)), response_body=json.dumps(data)), start=start)
def send(self, channel, msg, text): # use regular Vumi channel sending return Channel.get_type_from_code('VM').send(channel, msg, text)
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 send(self, channel, msg, text): # use regular Junebug channel sending return Channel.get_type_from_code('JN').send(channel, msg, text)
def send(self, channel, msg, text): connection = None # if the channel config has specified and override hostname use that, otherwise use settings callback_domain = channel.config.get( Channel.CONFIG_RP_HOSTNAME_OVERRIDE, None) if not callback_domain: callback_domain = channel.callback_domain # the event url Junebug will relay events to event_url = "http://%s%s" % ( callback_domain, reverse("handlers.junebug_handler", args=["event", channel.uuid]), ) is_ussd = Channel.get_type_from_code( channel.channel_type).category == ChannelType.Category.USSD # build our payload payload = {"event_url": event_url, "content": text} secret = channel.config.get(Channel.CONFIG_SECRET) if secret is not None: payload["event_auth_token"] = secret connection = USSDSession.objects.get_with_status_only( msg.connection_id) # make sure USSD responses are only valid for a short window response_expiration = timezone.now() - timedelta(seconds=180) external_id = None if msg.response_to_id and msg.created_on > response_expiration: external_id = Msg.objects.values_list( "external_id", flat=True).filter(pk=msg.response_to_id).first() # NOTE: Only one of `to` or `reply_to` may be specified, use external_id if we have it. if external_id: payload["reply_to"] = external_id else: payload["to"] = msg.urn_path payload["channel_data"] = { "continue_session": connection and not connection.should_end or False } log_url = channel.config[Channel.CONFIG_SEND_URL] start = time.time() event = HttpEvent("POST", log_url, json.dumps(payload)) headers = http_headers(extra={"Content-Type": "application/json"}) try: response = requests.post( channel.config[Channel.CONFIG_SEND_URL], verify=True, json=payload, timeout=15, headers=headers, auth=(channel.config[Channel.CONFIG_USERNAME], channel.config[Channel.CONFIG_PASSWORD]), ) event.status_code = response.status_code event.response_body = response.text except Exception as e: raise SendException(str(e), event=event, start=start) if not (200 <= response.status_code < 300): raise SendException("Received a non 200 response %d from Junebug" % response.status_code, event=event, start=start) data = response.json() if is_ussd and connection and connection.should_end: connection.close() try: message_id = data["result"]["message_id"] Channel.success(channel, msg, WIRED, start, event=event, external_id=message_id) except KeyError as e: raise SendException( "Unable to read external message_id: %r" % (e, ), event=HttpEvent("POST", log_url, request_body=json.dumps(json.dumps(payload)), response_body=json.dumps(data)), start=start, )
def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) is_support = self.request.user.groups.filter(name="Customer Support").first() end = timezone.now() begin = end - timedelta(days=30) begin = self.request.GET.get("begin", datetime.strftime(begin, "%Y-%m-%d")) end = self.request.GET.get("end", datetime.strftime(end, "%Y-%m-%d")) direction = self.request.GET.get("direction", "IO") if begin and end: orgs = [] org = self.derive_org() if org: orgs = Org.objects.filter(Q(id=org.id) | Q(parent=org)) count_types = [] if "O" in direction: count_types = [ChannelCount.OUTGOING_MSG_TYPE, ChannelCount.OUTGOING_IVR_TYPE] if "I" in direction: count_types += [ChannelCount.INCOMING_MSG_TYPE, ChannelCount.INCOMING_IVR_TYPE] # get all our counts for that period daily_counts = ( ChannelCount.objects.filter(count_type__in=count_types) .filter(day__gte=begin) .filter(day__lte=end) .exclude(channel__org=None) ) if orgs: daily_counts = daily_counts.filter(channel__org__in=orgs) context["orgs"] = list( daily_counts.values("channel__org", "channel__org__name") .order_by("-count_sum") .annotate(count_sum=Sum("count"))[:12] ) channel_types = ( ChannelCount.objects.filter(count_type__in=count_types) .filter(day__gte=begin) .filter(day__lte=end) .exclude(channel__org=None) ) if orgs or not is_support: channel_types = channel_types.filter(channel__org__in=orgs) channel_types = list( channel_types.values("channel__channel_type").order_by("-count_sum").annotate(count_sum=Sum("count")) ) # populate the channel names pie = [] for channel_type in channel_types[0:6]: channel_type["channel__name"] = Channel.get_type_from_code(channel_type["channel__channel_type"]).name pie.append(channel_type) other_count = 0 for channel_type in channel_types[6:]: other_count += channel_type["count_sum"] if other_count: pie.append(dict(channel__name="Other", count_sum=other_count)) context["channel_types"] = pie context["begin"] = datetime.strptime(begin, "%Y-%m-%d").date() context["end"] = datetime.strptime(end, "%Y-%m-%d").date() context["direction"] = direction return context
def send(self, channel, msg, text): connection = None # if the channel config has specified and override hostname use that, otherwise use settings callback_domain = channel.config.get(Channel.CONFIG_RP_HOSTNAME_OVERRIDE, None) if not callback_domain: callback_domain = channel.callback_domain # the event url Junebug will relay events to event_url = "http://%s%s" % ( callback_domain, reverse("handlers.junebug_handler", args=["event", channel.uuid]), ) is_ussd = Channel.get_type_from_code(channel.channel_type).category == ChannelType.Category.USSD # build our payload payload = {"event_url": event_url, "content": text} secret = channel.config.get(Channel.CONFIG_SECRET) if secret is not None: payload["event_auth_token"] = secret connection = USSDSession.objects.get_with_status_only(msg.connection_id) # make sure USSD responses are only valid for a short window response_expiration = timezone.now() - timedelta(seconds=180) external_id = None if msg.response_to_id and msg.created_on > response_expiration: external_id = Msg.objects.values_list("external_id", flat=True).filter(pk=msg.response_to_id).first() # NOTE: Only one of `to` or `reply_to` may be specified, use external_id if we have it. if external_id: payload["reply_to"] = external_id else: payload["to"] = msg.urn_path payload["channel_data"] = {"continue_session": connection and not connection.should_end or False} log_url = channel.config[Channel.CONFIG_SEND_URL] start = time.time() event = HttpEvent("POST", log_url, json.dumps(payload)) headers = http_headers(extra={"Content-Type": "application/json"}) try: response = requests.post( channel.config[Channel.CONFIG_SEND_URL], verify=True, json=payload, timeout=15, headers=headers, auth=(channel.config[Channel.CONFIG_USERNAME], channel.config[Channel.CONFIG_PASSWORD]), ) event.status_code = response.status_code event.response_body = response.text except Exception as e: raise SendException(str(e), event=event, start=start) if not (200 <= response.status_code < 300): raise SendException( "Received a non 200 response %d from Junebug" % response.status_code, event=event, start=start ) data = response.json() if is_ussd and connection and connection.should_end: connection.close() try: message_id = data["result"]["message_id"] Channel.success(channel, msg, WIRED, start, event=event, external_id=message_id) except KeyError as e: raise SendException( "Unable to read external message_id: %r" % (e,), event=HttpEvent( "POST", log_url, request_body=json.dumps(json.dumps(payload)), response_body=json.dumps(data) ), start=start, )
def send(self, channel, msg, text): is_ussd = Channel.get_type_from_code( channel.channel_type).category == ChannelType.Category.USSD channel.config[ 'transport_name'] = 'ussd_transport' if is_ussd else 'mtech_ng_smpp_transport' session = None session_event = None in_reply_to = None if is_ussd: session = USSDSession.objects.get_with_status_only( msg.connection_id) if session and session.should_end: session_event = "close" else: session_event = "resume" if msg.response_to_id: in_reply_to = Msg.objects.values_list( 'external_id', flat=True).filter(pk=msg.response_to_id).first() payload = dict(message_id=msg.id, in_reply_to=in_reply_to, session_event=session_event, to_addr=msg.urn_path, from_addr=channel.address, content=text, transport_name=channel.config['transport_name'], transport_type='ussd' if is_ussd else 'sms', transport_metadata={}, helper_metadata={}) payload = json.dumps(payload) headers = http_headers(extra={'Content-Type': 'application/json'}) api_url_base = channel.config.get('api_url', Channel.VUMI_GO_API_URL) url = "%s/%s/messages.json" % (api_url_base, channel.config['conversation_key']) event = HttpEvent('PUT', url, json.dumps(payload)) start = time.time() validator = URLValidator() validator(url) try: response = requests.put(url, data=payload, headers=headers, timeout=30, auth=(channel.config['account_key'], channel.config['access_token'])) event.status_code = response.status_code event.response_body = response.text except Exception as e: raise SendException(six.text_type(e), event=event, start=start) if response.status_code not in (200, 201): # this is a fatal failure, don't retry fatal = response.status_code == 400 # if this is fatal due to the user opting out, stop them if response.text and response.text.find('has opted out') >= 0: contact = Contact.objects.get(id=msg.contact) contact.stop(contact.modified_by) fatal = True raise SendException("Got non-200 response [%d] from API" % response.status_code, event=event, fatal=fatal, start=start) # parse our response body = response.json() external_id = body.get('message_id', '') if is_ussd and session and session.should_end: session.close() Channel.success(channel, msg, WIRED, start, event=event, external_id=external_id)