diff --git a/kairon/chat/handlers/channels/business_messages/business_messages.py b/kairon/chat/handlers/channels/business_messages.py similarity index 81% rename from kairon/chat/handlers/channels/business_messages/business_messages.py rename to kairon/chat/handlers/channels/business_messages.py index c476bed31..c7e6f47be 100644 --- a/kairon/chat/handlers/channels/business_messages/business_messages.py +++ b/kairon/chat/handlers/channels/business_messages.py @@ -22,21 +22,16 @@ from kairon.chat.agent_processor import AgentProcessor -path_to_service_account_key = 'kairon/chat/handlers/channels/business_messages/service_account_key.json' - - class BusinessMessagesHandler(InputChannel): def __init__(self, bot: Text, user: User, request: Request): self.bot = bot self.user = user self.request = request + self.channel_config = {} async def validate(self): - business_message_conf = ChatDataProcessor.get_channel_config("business_messages", self.bot, - mask_characters=False) - - partner_key = business_message_conf["config"]["private_key_id"] + partner_key = self.channel_config["private_key_id"] generated_signature = base64.b64encode(hmac.new(partner_key.encode(), msg=await self.request.body(), digestmod=hashlib.sha512).digest()).decode('UTF-8') google_signature = self.request.headers.get('x-goog-signature') @@ -50,16 +45,10 @@ async def handle_message(self): if 'secret' in request_body: return request_body.get('secret') - await self.validate() business_message_conf = ChatDataProcessor.get_channel_config("business_messages", self.bot, mask_characters=False) - credentials_json = { - "type": "service_account", - "private_key_id": business_message_conf["config"]["private_key_id"], - "private_key": business_message_conf["config"]["private_key"], - "client_email": business_message_conf["config"]["client_email"], - "client_id": business_message_conf["config"]["client_id"] - } + self.channel_config = business_message_conf['config'] + await self.validate() print(request_body) metadata = self.get_metadata(self.request) or {} metadata.update({"is_integration_user": True, "bot": self.bot, "account": self.user.account, @@ -71,10 +60,11 @@ async def handle_message(self): message = request_body['message']['text'] conversation_id = request_body['conversationId'] message_id = request_body['message']['messageId'] - business_messages = BusinessMessages(credentials_json) + business_messages = BusinessMessages(self.channel_config) await business_messages.handle_user_message(text=message, sender_id=self.user.email, metadata=metadata, conversation_id=conversation_id, bot=self.bot, message_id=message_id) + logger.debug("Business Messages Request: " + str(request_body)) return {"status": "OK"} @staticmethod @@ -84,30 +74,32 @@ def check_message_create_time(create_time: str): current_time = datetime.utcnow() message_time = datetime.strptime(create_time, '%Y-%m-%dT%H:%M:%S.%fZ') time_difference = current_time - message_time - print(time_difference) return True if time_difference.total_seconds() < 5 else False class BusinessMessages: - def __init__(self, credentials_json: Dict): - self.credentials_json = credentials_json + def __init__(self, channel_config: Dict): + self.channel_config = channel_config @classmethod def name(cls) -> Text: return "business_messages" - @staticmethod - def write_json_to_file(json_code, file_path): - import json - - with open(file_path, 'w') as json_file: - json.dump(json_code, json_file, indent=2) + def get_credentials(self): + credentials_json = { + "type": "service_account", + "private_key_id": self.channel_config["private_key_id"], + "private_key": self.channel_config["private_key"], + "client_email": self.channel_config["client_email"], + "client_id": self.channel_config["client_id"] + } + return credentials_json def get_business_message_credentials(self): - self.write_json_to_file(self.credentials_json, path_to_service_account_key) - credentials = ServiceAccountCredentials.from_json_keyfile_name( - path_to_service_account_key, + credentials = self.get_credentials() + credentials = ServiceAccountCredentials.from_json_keyfile_dict( + credentials, scopes=['https://www.googleapis.com/auth/businessmessages']) return credentials @@ -117,16 +109,14 @@ async def handle_user_message( ) -> None: user_msg = UserMessage(text=text, message_id=message_id, input_channel=self.name(), sender_id=sender_id, metadata=metadata) - message = "No Response" try: response = await self.process_message(bot, user_msg) print(response) - message = response['response'][0]['text'] - except Exception: - logger.exception( - "Exception when trying to handle webhook for business message." - ) - await self.send_message(message=message, conversation_id=conversation_id) + message = response['response'][0].get('text') + except Exception as e: + raise Exception(f"Exception when trying to handle webhook for business message: {str(e)}") + if message: + await self.send_message(message=message, conversation_id=conversation_id) @staticmethod async def process_message(bot: str, user_message: UserMessage): @@ -146,19 +136,14 @@ async def send_message(self, message: Text, conversation_id: Text): credentials = self.get_business_message_credentials() client = bm_client.BusinessmessagesV1(credentials=credentials) - create_request = BusinessmessagesConversationsEventsCreateRequest( - eventId=str(uuid.uuid4().int), - businessMessagesEvent=BusinessMessagesEvent( - representative=BusinessMessagesRepresentative( - representativeType=BusinessMessagesRepresentative.RepresentativeTypeValueValuesEnum.BOT - ), - eventType=BusinessMessagesEvent.EventTypeValueValuesEnum.TYPING_STARTED - ), - parent='conversations/' + conversation_id) + self.trigger_start_typing_event(conversation_id, client) - bm_client.BusinessmessagesV1.ConversationsEventsService( - client=client).Create(request=create_request) + self.send_google_business_message(message, conversation_id, client) + + self.trigger_stop_typing_event(conversation_id, client) + @staticmethod + def send_google_business_message(message, conversation_id, client): message_obj = BusinessMessagesMessage( messageId=str(uuid.uuid4().int), representative=BusinessMessagesRepresentative( @@ -173,6 +158,23 @@ async def send_message(self, message: Text, conversation_id: Text): bm_client.BusinessmessagesV1.ConversationsMessagesService( client=client).Create(request=create_request) + @staticmethod + def trigger_start_typing_event(conversation_id, client): + create_request = BusinessmessagesConversationsEventsCreateRequest( + eventId=str(uuid.uuid4().int), + businessMessagesEvent=BusinessMessagesEvent( + representative=BusinessMessagesRepresentative( + representativeType=BusinessMessagesRepresentative.RepresentativeTypeValueValuesEnum.BOT + ), + eventType=BusinessMessagesEvent.EventTypeValueValuesEnum.TYPING_STARTED + ), + parent='conversations/' + conversation_id) + + bm_client.BusinessmessagesV1.ConversationsEventsService( + client=client).Create(request=create_request) + + @staticmethod + def trigger_stop_typing_event(conversation_id, client): create_request = BusinessmessagesConversationsEventsCreateRequest( eventId=str(uuid.uuid4().int), businessMessagesEvent=BusinessMessagesEvent( diff --git a/kairon/chat/handlers/channels/business_messages/__init__.py b/kairon/chat/handlers/channels/business_messages/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/kairon/chat/handlers/channels/business_messages/service_account_key.json b/kairon/chat/handlers/channels/business_messages/service_account_key.json deleted file mode 100644 index d8b178cb4..000000000 --- a/kairon/chat/handlers/channels/business_messages/service_account_key.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "service_account", - "private_key_id": "fa006e13b1e17eddf3990eede45ca6111eb74945", - "private_key": "test_private_key", - "client_email": "solution-provider@gbc-mahesh-mxqtkk9.iam.testaccount.com", - "client_id": "102056160806575769486" -} \ No newline at end of file diff --git a/kairon/chat/handlers/channels/factory.py b/kairon/chat/handlers/channels/factory.py index f52b46011..a375756a5 100644 --- a/kairon/chat/handlers/channels/factory.py +++ b/kairon/chat/handlers/channels/factory.py @@ -1,6 +1,6 @@ from typing import Text -from kairon.chat.handlers.channels.business_messages.business_messages import BusinessMessagesHandler +from kairon.chat.handlers.channels.business_messages import BusinessMessagesHandler from kairon.chat.handlers.channels.hangouts import HangoutsHandler from kairon.chat.handlers.channels.messenger import MessengerHandler, InstagramHandler from kairon.chat.handlers.channels.msteams import MSTeamsHandler diff --git a/tests/integration_test/chat_service_test.py b/tests/integration_test/chat_service_test.py index 5f25fc4a1..c600a3b6c 100644 --- a/tests/integration_test/chat_service_test.py +++ b/tests/integration_test/chat_service_test.py @@ -292,10 +292,88 @@ def test_business_messages_invalid_auth(): assert actual == {"data": None, "success": False, "error_code": 401, "message": "Could not validate credentials"} -@patch("kairon.chat.handlers.channels.business_messages.business_messages.BusinessMessages.process_message") -@patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name') +def test_business_messages_with_secret(): + response = client.post( + f"/api/bot/business_messages/{bot}/{token}", + headers={"Authorization": "Bearer Test"}, + json={"secret": "34983948"} + ) + actual = response.json() + assert actual == "34983948" + + +@patch("kairon.chat.handlers.channels.business_messages.BusinessMessagesHandler.check_message_create_time") +@patch("kairon.chat.handlers.channels.business_messages.BusinessMessages.process_message") +@patch('businessmessages.businessmessages_v1_client.BusinessmessagesV1') +def test_business_messages_with_exception(mock_business_messages, mock_process_message, + mock_check_message_create_time): + mock_check_message_create_time.return_value = True + mock_business_messages.return_value = {} + mock_process_message.side_effect = Exception("invalid user message") + with pytest.raises(Exception, + match="Exception when trying to handle webhook for business message: invalid user message"): + client.post( + f"/api/bot/business_messages/{bot}/{token}", + headers={"Authorization": "Bearer Test"}, + json={"message": { + "name": "conversations/24ab463a-a6bf-4049-b49e-cc05fb1dc384/messages/5979C5-325C-4700-BF5A-0156C39C541", + "text": "Hello!", + "createTime": "2023-12-04T06:30:46.034290Z", + "messageId": "5979C547-325C-4700-BF5A-0156C39C5641"}, + "context": { + "placeId": "", + "userInfo": { + "displayName": "Mahesh Sattala", + "userDeviceLocale": "en-IN" + }, + "resolvedLocale": "en" + }, + "sendTime": "2023-12-04T06:30:46.662594Z", + "conversationId": "24ab463a-a6bf-4056-b49e-aa05fb1dc384", + "requestId": "5979C547-325C-4700-BF5A-0156C45C1541", + "agent": "brands/bd7e3fe0-3c3e-4b3e-4759-6e46ac0412a5/agents/3cf91834-3b5e-4c4b-a632-9575f0cc3444" + } + ) + + +@patch("kairon.chat.handlers.channels.business_messages.BusinessMessagesHandler.check_message_create_time") +@patch("kairon.chat.handlers.channels.business_messages.BusinessMessages.process_message") +def test_business_messages_without_message(mock_process_message, mock_check_message_create_time): + mock_check_message_create_time.return_value = True + mock_process_message.return_value = {'response': [{'text': None}]} + response = client.post( + f"/api/bot/business_messages/{bot}/{token}", + headers={"Authorization": "Bearer Test"}, + json={"message": { + "name": "conversations/24ab463a-a6bf-4049-b49e-cc05fb1dc384/messages/5979C5-325C-4700-BF5A-0156C39C541", + "text": "Hello!", + "createTime": "2023-12-04T06:30:46.034290Z", + "messageId": "5979C547-325C-4700-BF5A-0156C39C5641"}, + "context": { + "placeId": "", + "userInfo": { + "displayName": "Mahesh Sattala", + "userDeviceLocale": "en-IN" + }, + "resolvedLocale": "en" + }, + "sendTime": "2023-12-04T06:30:46.662594Z", + "conversationId": "24ab463a-a6bf-4056-b49e-aa05fb1dc384", + "requestId": "5979C547-325C-4700-BF5A-0156C45C1541", + "agent": "brands/bd7e3fe0-3c3e-4b3e-4759-6e46ac0412a5/agents/3cf91834-3b5e-4c4b-a632-9575f0cc3444" + } + ) + actual = response.json() + assert actual == {"status": "OK"} + + +@patch("kairon.chat.handlers.channels.business_messages.BusinessMessagesHandler.check_message_create_time") +@patch("kairon.chat.handlers.channels.business_messages.BusinessMessages.process_message") +@patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_dict') @patch('businessmessages.businessmessages_v1_client.BusinessmessagesV1') -def test_business_messages_with_valid_data(mock_business_messages, mock_credentials, mock_process_message): +def test_business_messages_with_valid_data(mock_business_messages, mock_credentials, mock_process_message, + mock_check_message_create_time): + mock_check_message_create_time.return_value = True mock_credentials.return_value = {} mock_business_messages.return_value = {} mock_process_message.return_value = {'response': [{'text': 'How may I help you!'}]} diff --git a/tests/unit_test/chat/chat_test.py b/tests/unit_test/chat/chat_test.py index 291ddeee8..c93f321b7 100644 --- a/tests/unit_test/chat/chat_test.py +++ b/tests/unit_test/chat/chat_test.py @@ -421,6 +421,34 @@ def __mock_endpoint(*args): "test", "test") + @responses.activate + def test_save_channel_config_business_messages(self): + def __mock_endpoint(*args): + return f"https://test@test.com/api/bot/business_messages/tests/test" + + with patch('kairon.shared.data.utils.DataUtility.get_channel_endpoint', __mock_endpoint): + channel_endpoint = ChatDataProcessor.save_channel_config( + { + "connector_type": "business_messages", + "config": { + "type": "service_account", + "private_key_id": "fa006e13b1e17eddf3990eede45ca6111eb74945", + "private_key": "test_private_key", + "client_email": "provider@gbc-mahesh.iam.testaccount.com", + "client_id": "102056160806575769486" + } + }, + "test", + "test") + assert channel_endpoint == "https://test@test.com/api/bot/business_messages/tests/test" + business_messages_config = ChatDataProcessor.get_channel_config("business_messages", + "test") + assert business_messages_config['config'] == {'type': 'service_account', + 'private_key_id': 'fa006e13b1e17eddf3990eede45ca6111eb*****', + 'private_key': 'test_privat*****', + 'client_email': 'provider@gbc-mahesh.iam.testaccoun*****', + 'client_id': '1020561608065757*****'} + @mock.patch('kairon.shared.utils.MongoClient', autospec=True) def test_fetch_session_history_error(self, mock_mongo): mock_mongo.side_effect = ServerSelectionTimeoutError("Failed to retrieve conversation: Failed to connect")