From 17e2627d6329468d7ee0097e4ee39d266cf8c922 Mon Sep 17 00:00:00 2001 From: Hitesh Ghuge <41512916+hiteshghuge@users.noreply.github.com> Date: Tue, 13 Feb 2024 17:19:53 +0530 Subject: [PATCH] - Adding instagram feature to reply on reel post (#1132) * - Adding instagram feature to reply on reel post * - Read static comment from environment if not configured --------- Co-authored-by: hghuge --- kairon/chat/handlers/channels/messenger.py | 45 +++++++- kairon/shared/chat/data_objects.py | 3 + kairon/shared/chat/processor.py | 7 ++ metadata/integrations.yml | 2 + system.yaml | 3 +- tests/integration_test/chat_service_test.py | 117 +++++++++++++++++++- tests/testing_data/system.yaml | 3 +- tests/unit_test/chat/chat_test.py | 47 ++++++++ 8 files changed, 222 insertions(+), 5 deletions(-) diff --git a/kairon/chat/handlers/channels/messenger.py b/kairon/chat/handlers/channels/messenger.py index d6f7b3906..f14b754cd 100644 --- a/kairon/chat/handlers/channels/messenger.py +++ b/kairon/chat/handlers/channels/messenger.py @@ -39,7 +39,10 @@ def __init__( self.last_message: Dict[Text, Any] = {} def get_user_id(self) -> Text: - return self.last_message.get("sender", {}).get("id", "") + sender_id = self.last_message.get("sender", {}).get("id", "") + if sender_id == '': + sender_id = self.last_message.get("value", {}).get("from", {}).get("id", "") + return sender_id @staticmethod def _is_audio_message(message: Dict[Text, Any]) -> bool: @@ -77,6 +80,10 @@ def _is_file_message(message: Dict[Text, Any]) -> bool: and message["message"]["attachments"][0]["type"] == "file" ) + @staticmethod + def _is_comment(message: Dict[Text, Any]) -> bool: + return message.get("field", "") == "comments" + @staticmethod def _is_user_message(message: Dict[Text, Any]) -> bool: """Check if the message is a message from the user""" @@ -97,12 +104,16 @@ def _is_quick_reply_message(message: Dict[Text, Any]) -> bool: async def handle(self, payload: Dict, metadata: Optional[Dict[Text, Any]], bot: str) -> None: for entry in payload["entry"]: - for message in entry["messaging"]: + for message in entry.get("messaging", []): self.last_message = message if message.get("message"): return await self.message(message, metadata, bot) elif message.get("postback"): return await self.postback(message, metadata, bot) + for change in entry.get("changes",[]): + self.last_message = change + if change.get("value"): + return await self.comment(change, metadata, bot) async def message( self, message: Dict[Text, Any], metadata: Optional[Dict[Text, Any]], bot: str @@ -144,6 +155,20 @@ async def postback( text = message["postback"]["payload"] await self._handle_user_message(text, self.get_user_id(), metadata, bot) + async def comment( + self, message: Dict[Text, Any], metadata: Optional[Dict[Text, Any]], bot: str + ): + if self._is_comment(message): + static_comment_reply = ChatDataProcessor.get_instagram_static_comment(bot=bot) + text = message["value"]["text"] + parent_id = message.get("value", {}).get("parent_id", None) + comment_id = message["value"]["id"] + user = message["value"]["from"]["username"] + metadata["comment_id"] = comment_id + metadata["static_comment_reply"] = f"@{user} {static_comment_reply}" + if not parent_id: + await self._handle_user_message(text, self.get_user_id(), metadata, bot) + async def _handle_user_message( self, text: Text, sender_id: Text, metadata: Optional[Dict[Text, Any]], bot: str ) -> None: @@ -156,6 +181,9 @@ async def _handle_user_message( text, out_channel, sender_id, input_channel=self.name(), metadata=metadata ) await out_channel.send_action(sender_id, sender_action="typing_on") + + if metadata.get("comment_id"): + await out_channel.reply_on_comment(**metadata) # noinspection PyBroadException try: await self.process_message(bot, user_msg) @@ -199,6 +227,19 @@ async def send_text_message( for message_part in text.strip().split("\n\n"): self.send(recipient_id, FBText(text=message_part)) + async def reply_on_comment( + self, comment_id: Text, bot: Text, **kwargs: Any + ): + body = {} + _r = self.messenger_client.session.post( + '{graph_url}/{comment_id}/replies?message={message}'. + format(graph_url=self.messenger_client.graph_url, + comment_id=comment_id, + message=kwargs.get("static_comment_reply")), + params=self.messenger_client.auth_args, + json=body + ) + async def send_image_url( self, recipient_id: Text, image: Text, **kwargs: Any ) -> None: diff --git a/kairon/shared/chat/data_objects.py b/kairon/shared/chat/data_objects.py index 875b2f3e5..1fcb80ff8 100644 --- a/kairon/shared/chat/data_objects.py +++ b/kairon/shared/chat/data_objects.py @@ -37,6 +37,9 @@ def validate(self, clean=True): }) Utility.register_telegram_webhook(Utility.decrypt_message(self.config['access_token']), webhook_url) + if self.connector_type == "instagram" and self.config.get("static_comment_reply") is None: + self.config["static_comment_reply"] = Utility.environment["channels"]["instagram"]["static_comment_reply"] + @auditlogger.log @push_notification.apply diff --git a/kairon/shared/chat/processor.py b/kairon/shared/chat/processor.py index 54df9fcff..3ffb2d1d2 100644 --- a/kairon/shared/chat/processor.py +++ b/kairon/shared/chat/processor.py @@ -158,3 +158,10 @@ def save_whatsapp_audit_log(status_data: Dict, bot: Text, user: Text, channel_ty user=user, campaign_id=campaign_id ).save() + + @staticmethod + def get_instagram_static_comment(bot: str) -> str: + channel = ChatDataProcessor.get_channel_config(bot=bot, connector_type="instagram", mask_characters=False) + comment_response = channel.get("config", {}).get("static_comment_reply") + return comment_response + diff --git a/metadata/integrations.yml b/metadata/integrations.yml index 5a32f237b..645ced94e 100644 --- a/metadata/integrations.yml +++ b/metadata/integrations.yml @@ -46,6 +46,8 @@ channels: - app_secret - page_access_token - verify_token + optional_fields: + - static_comment_reply: Thanks for reaching us, please check your inbox whatsapp: required_fields: - app_secret diff --git a/system.yaml b/system.yaml index c5834923f..7539981a0 100644 --- a/system.yaml +++ b/system.yaml @@ -197,7 +197,8 @@ channels: partner_id: ${360_DIALOG_PARTNER_ID} partner_username: ${360_DIALOG_PARTNER_USERNAME} partner_password: ${360DIALOG_PARTNER_PASSWORD} - + instagram: + static_comment_reply: ${INSTA_STATIC_COMMENT_REPLY:Thanks for reaching us, please check your inbox} llm: faq: ${LLM_FAQ_TYPE:GPT3_FAQ_EMBED} key: ${TEMPLATE_LLM_KEY} diff --git a/tests/integration_test/chat_service_test.py b/tests/integration_test/chat_service_test.py index 2ad5e94ce..f32109a8b 100644 --- a/tests/integration_test/chat_service_test.py +++ b/tests/integration_test/chat_service_test.py @@ -19,7 +19,7 @@ from kairon.api.models import RegisterAccount from kairon.chat.agent.agent import KaironAgent from kairon.chat.agent.message_processor import KaironMessageProcessor -from kairon.chat.handlers.channels.messenger import MessengerHandler +from kairon.chat.handlers.channels.messenger import MessengerHandler, InstagramHandler from kairon.chat.server import app from kairon.chat.utils import ChatUtils from kairon.exceptions import AppException @@ -153,6 +153,16 @@ def __mock_endpoint(*args): } }, bot, user="test@chat.com") + +ChatDataProcessor.save_channel_config({"connector_type": "instagram", + "config": { + "app_secret": "cdb69bc72e2ccb7a869f20cbb6b0229a", + "page_access_token": "EAAGa50I7D7cBAJ4AmXOhYAeOOZAyJ9fxOclQmn52hBwrOJJWBOxuJNXqQ2uN667z4vLekSEqnCQf41hcxKVZAe2pAZBrZCTENEj1IBe1CHEcG7J33ZApED9Tj9hjO5tE13yckNa8lP3lw2IySFqeg6REJR3ZCJUvp2h03PQs4W5vNZBktWF3FjQYz5vMEXLPzAFIJcZApBtq9wZDZD", + "verify_token": "kairon-instagram-token", + } + }, + bot, user="test@chat.com") + responses.stop() @@ -2898,3 +2908,108 @@ def test_chat_with_chatwoot_agent_outof_workinghours(mock_validatebusiness, mock assert actual["data"]["agent_handoff"][ "businessworking"] == "We are unavailable at the moment. In case of any query related to Sales, gifting or enquiry of order, please connect over following whatsapp number +912929393 ." + +@responses.activate +def test_instagram_comment(): + def _mock_validate_hub_signature(*args, **kwargs): + return True + + message = "@kairon_user_123 Thanks for reaching us, please check your inbox" + access_token = "EAAGa50I7D7cBAJ4AmXOhYAeOOZAyJ9fxOclQmn52hBwrOJJWBOxuJNXqQ2uN667z4vLekSEqnCQf41hcxKVZAe2pAZBrZCTENEj1IBe1CHEcG7J33ZApED9Tj9hjO5tE13yckNa8lP3lw2IySFqeg6REJR3ZCJUvp2h03PQs4W5vNZBktWF3FjQYz5vMEXLPzAFIJcZApBtq9wZDZD" + responses.add( + "POST", f"https://graph.facebook.com/v2.12/18009764417219041/replies?message={message}&access_token={access_token}", json={} + ) + responses.add( + "POST", f"https://graph.facebook.com/v2.12/me/messages?access_token={access_token}", json={} + ) + + with mock.patch.object(EndpointConfig, "request") as mock_action_execution: + mock_action_execution.return_value = {"responses": [{"response": "Welcome to kairon!"}], "events": []} + + with patch.object(InstagramHandler, "validate_hub_signature", _mock_validate_hub_signature): + response = client.post( + f"/api/bot/instagram/{bot}/{token}", + headers={"hub.verify_token": "valid"}, + json={ + "entry": [ + { + "id": "17841456706109718", + "time": 1707144192, + "changes": [ + { + "value": { + "from": { + "id": "6489091794524304", + "username": "kairon_user_123" + }, + "media": { + "id": "18013303267972611", + "media_product_type": "REELS" + }, + "id": "18009764417219041", + "text": "Hi" + }, + "field": "comments" + } + ] + } + ], + "object": "instagram" + }) + time.sleep(5) + mock_action_execution.assert_awaited_once() + + actual = response.json() + print(f"Actual response for instagram is {actual}") + assert actual == 'success' + assert MeteringProcessor.get_metric_count(user['account'], metric_type=MetricType.prod_chat, + channel_type="instagram") > 0 + + +@responses.activate +def test_instagram_comment_with_parent_comment(): + def _mock_validate_hub_signature(*args, **kwargs): + return True + + with mock.patch.object(EndpointConfig, "request") as mock_action_execution: + mock_action_execution.return_value = {"responses": [{"response": "Welcome to kairon!"}], "events": []} + + with patch.object(InstagramHandler, "validate_hub_signature", _mock_validate_hub_signature): + response = client.post( + f"/api/bot/instagram/{bot}/{token}", + headers={"hub.verify_token": "valid"}, + json={ + "entry": [ + { + "id": "17841456706109718", + "time": 1707144192, + "changes": [ + { + "value": { + "from": { + "id": "6489091794524304", + "username": "_hdg_photography" + }, + "media": { + "id": "18013303267972611", + "media_product_type": "REELS" + }, + "id": "18009764417219042", + "parent_id": "18009764417219041", + "text": "Hi" + }, + "field": "comments" + } + ] + } + ], + "object": "instagram" + }) + + actual = response.json() + print(f"Actual response for instagram is {actual}") + assert actual == 'success' + assert MeteringProcessor.get_metric_count(user['account'], metric_type=MetricType.prod_chat, + channel_type="instagram") > 0 + + diff --git a/tests/testing_data/system.yaml b/tests/testing_data/system.yaml index 0c7018a06..dad83c3ed 100644 --- a/tests/testing_data/system.yaml +++ b/tests/testing_data/system.yaml @@ -195,7 +195,8 @@ channels: partner_id: ${360_DIALOG_PARTNER_ID} partner_username: ${360_DIALOG_PARTNER_USERNAME} partner_password: ${360DIALOG_PARTNER_PASSWORD} - + instagram: + static_comment_reply: ${INSTA_STATIC_COMMENT_REPLY:Thanks for reaching us, please check your inbox} llm: faq: ${LLM_FAQ_TYPE:GPT3_FAQ_EMBED} diff --git a/tests/unit_test/chat/chat_test.py b/tests/unit_test/chat/chat_test.py index ee9bd3e01..05391b63c 100644 --- a/tests/unit_test/chat/chat_test.py +++ b/tests/unit_test/chat/chat_test.py @@ -606,3 +606,50 @@ async def test_base_channel(self): with pytest.raises(NotImplementedError): await ChannelHandlerBase().handle_message() + + def test_save_channel_config_insta_with_default_comment_reply(self, monkeypatch): + bot = '5e564fbcdcf0d5fad89e3acd' + + def _get_integration_token(*args, **kwargs): + return "eyJhbGciOiJIUzI1NiI.sInR5cCI6IkpXVCJ9.TXXmZ4-rMKQZMLwS104JsvsR0XPg4xBt2UcT4x4HgLY", "" + + monkeypatch.setattr(Authentication, "generate_integration_token", _get_integration_token) + channel_url = ChatDataProcessor.save_channel_config({ + "connector_type": "instagram", "config": { + "app_secret": "cdb69bc72e2ccb7a869f20cbb6b0229a", + "page_access_token": "EAAGa50I7D7cBAJ4AmXOhYAeOOZAyJ9fxOclQmn52hBwrOJJWBOxuJNXqQ2uN667z4vLekSEqnCQf41hcxKVZAe2pAZBrZCTENEj1IBe1CHEcG7J33ZApED9Tj9hjO5tE13yckNa8lP3lw2IySFqeg6REJR3ZCJUvp2h03PQs4W5vNZBktWF3FjQYz5vMEXLPzAFIJcZApBtq9wZDZD", + "verify_token": "kairon-instagram-token", + }}, bot, "test@chat.com") + insta_webhook = ChatDataProcessor.get_channel_endpoint("instagram", bot) + hashcode = channel_url.split("/", -1)[-1] + dbhashcode = insta_webhook.split("/", -1)[-1] + assert hashcode == dbhashcode + + insta = ChatDataProcessor.get_channel_config("instagram", bot, False) + + static_comment_reply_actual = insta.get("config", {}).get("static_comment_reply") + assert "Thanks for reaching us, please check your inbox" == static_comment_reply_actual + + def test_save_channel_config_insta_with_custom_comment_reply(self, monkeypatch): + bot = '5e564fbcdcf0d5fad89e3acd' + + def _get_integration_token(*args, **kwargs): + return "eyJhbGciOiJIUzI1NiI.sInR5cCI6IkpXVCJ9.TXXmZ4-rMKQZMLwS104JsvsR0XPg4xBt2UcT4x4HgLY", "" + + monkeypatch.setattr(Authentication, "generate_integration_token", _get_integration_token) + channel_url = ChatDataProcessor.save_channel_config({ + "connector_type": "instagram", "config": { + "app_secret": "cdb69bc72e2ccb7a869f20cbb6b0229a", + "page_access_token": "EAAGa50I7D7cBAJ4AmXOhYAeOOZAyJ9fxOclQmn52hBwrOJJWBOxuJNXqQ2uN667z4vLekSEqnCQf41hcxKVZAe2pAZBrZCTENEj1IBe1CHEcG7J33ZApED9Tj9hjO5tE13yckNa8lP3lw2IySFqeg6REJR3ZCJUvp2h03PQs4W5vNZBktWF3FjQYz5vMEXLPzAFIJcZApBtq9wZDZD", + "verify_token": "kairon-instagram-token", + "static_comment_reply": "Dhanyawad" + }}, bot, "test@chat.com") + insta_webhook = ChatDataProcessor.get_channel_endpoint("instagram", bot) + hashcode = channel_url.split("/", -1)[-1] + dbhashcode = insta_webhook.split("/", -1)[-1] + assert hashcode == dbhashcode + + insta = ChatDataProcessor.get_channel_config("instagram", bot, False) + + static_comment_reply_actual = insta.get("config", {}).get("static_comment_reply") + assert "Dhanyawad" == static_comment_reply_actual \ No newline at end of file