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 test_send_ussd_continue_and_end_session(self): flow = self.get_flow('ussd_session_end') contact = self.create_contact("Joe", "+250788383383") try: settings.SEND_MESSAGES = True with patch('requests.post') as mock: mock.return_value = MockResponse(200, json.dumps({ 'result': { 'message_id': '07033084-5cfd-4812-90a4-e4d24ffb6e3d', } })) flow.start([], [contact]) # our outgoing message msg = Msg.objects.filter(direction='O').order_by('id').last() self.assertEqual(msg.direction, 'O') self.assertTrue(msg.sent_on) self.assertEqual("07033084-5cfd-4812-90a4-e4d24ffb6e3d", msg.external_id) self.assertEqual(msg.connection.status, USSDSession.INITIATED) # reply and choose an option that doesn't have any destination thus needs to close the session USSDSession.handle_incoming(channel=self.channel, urn="+250788383383", content="4", date=timezone.now(), external_id="21345", message_id='vumi-message-id') # our outgoing message msg = Msg.objects.filter(direction='O').order_by('id').last() self.assertEqual(WIRED, msg.status) self.assertEqual(msg.direction, 'O') self.assertTrue(msg.sent_on) self.assertEqual("07033084-5cfd-4812-90a4-e4d24ffb6e3d", msg.external_id) self.assertEqual("vumi-message-id", msg.response_to.external_id) self.assertEqual(msg.connection.status, USSDSession.COMPLETED) self.assertTrue(isinstance(msg.connection.get_duration(), timedelta)) self.assertEqual(2, mock.call_count) # first outbound (session continued) call = mock.call_args_list[0] (args, kwargs) = call payload = kwargs['json'] self.assertIsNone(payload.get('reply_to')) self.assertEqual(payload.get('to'), "+250788383383") self.assertEqual(payload['channel_data'], { 'continue_session': True }) # second outbound (session ended) call = mock.call_args_list[1] (args, kwargs) = call payload = kwargs['json'] self.assertEqual(payload['reply_to'], 'vumi-message-id') self.assertEqual(payload.get('to'), None) self.assertEqual(payload['channel_data'], { 'continue_session': False }) self.clear_cache() finally: settings.SEND_MESSAGES = False
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 test_send_ussd_continue_and_end_session(self): flow = self.get_flow("ussd_session_end") contact = self.create_contact("Joe", "+250788383383") try: settings.SEND_MESSAGES = True with patch("requests.post") as mock: mock.return_value = MockResponse( 200, json.dumps({"result": {"message_id": "07033084-5cfd-4812-90a4-e4d24ffb6e3d"}}) ) flow.start([], [contact]) # our outgoing message msg = Msg.objects.filter(direction="O").order_by("id").last() self.assertEqual(msg.direction, "O") self.assertTrue(msg.sent_on) self.assertEqual("07033084-5cfd-4812-90a4-e4d24ffb6e3d", msg.external_id) self.assertEqual(msg.connection.status, USSDSession.INITIATED) # reply and choose an option that doesn't have any destination thus needs to close the session USSDSession.handle_incoming( channel=self.channel, urn="+250788383383", content="4", date=timezone.now(), external_id="21345", message_id="jn-message-id", ) # our outgoing message msg = Msg.objects.filter(direction="O").order_by("id").last() self.assertEqual(WIRED, msg.status) self.assertEqual(msg.direction, "O") self.assertTrue(msg.sent_on) self.assertEqual("07033084-5cfd-4812-90a4-e4d24ffb6e3d", msg.external_id) self.assertEqual("jn-message-id", msg.response_to.external_id) self.assertEqual(msg.connection.status, USSDSession.COMPLETED) self.assertTrue(isinstance(msg.connection.get_duration(), timedelta)) self.assertEqual(2, mock.call_count) # first outbound (session continued) call = mock.call_args_list[0] (args, kwargs) = call payload = kwargs["json"] self.assertIsNone(payload.get("reply_to")) self.assertEqual(payload.get("to"), "+250788383383") self.assertEqual(payload["channel_data"], {"continue_session": True}) # second outbound (session ended) call = mock.call_args_list[1] (args, kwargs) = call payload = kwargs["json"] self.assertEqual(payload["reply_to"], "jn-message-id") self.assertEqual(payload.get("to"), None) self.assertEqual(payload["channel_data"], {"continue_session": False}) self.clear_cache() finally: settings.SEND_MESSAGES = False
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)