Skip to content

Commit

Permalink
- Adding instagram feature to reply on reel post (#1132)
Browse files Browse the repository at this point in the history
* - Adding instagram feature to reply on reel post

* - Read static comment from environment if not configured

---------

Co-authored-by: hghuge <[email protected]>
  • Loading branch information
hiteshghuge and hghuge authored Feb 13, 2024
1 parent 49226b8 commit 17e2627
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 5 deletions.
45 changes: 43 additions & 2 deletions kairon/chat/handlers/channels/messenger.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"""
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions kairon/shared/chat/data_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions kairon/shared/chat/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

2 changes: 2 additions & 0 deletions metadata/integrations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion system.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
117 changes: 116 additions & 1 deletion tests/integration_test/chat_service_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -153,6 +153,16 @@ def __mock_endpoint(*args):
}
},
bot, user="[email protected]")

ChatDataProcessor.save_channel_config({"connector_type": "instagram",
"config": {
"app_secret": "cdb69bc72e2ccb7a869f20cbb6b0229a",
"page_access_token": "EAAGa50I7D7cBAJ4AmXOhYAeOOZAyJ9fxOclQmn52hBwrOJJWBOxuJNXqQ2uN667z4vLekSEqnCQf41hcxKVZAe2pAZBrZCTENEj1IBe1CHEcG7J33ZApED9Tj9hjO5tE13yckNa8lP3lw2IySFqeg6REJR3ZCJUvp2h03PQs4W5vNZBktWF3FjQYz5vMEXLPzAFIJcZApBtq9wZDZD",
"verify_token": "kairon-instagram-token",
}
},
bot, user="[email protected]")

responses.stop()


Expand Down Expand Up @@ -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


3 changes: 2 additions & 1 deletion tests/testing_data/system.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
47 changes: 47 additions & 0 deletions tests/unit_test/chat/chat_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, "[email protected]")
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, "[email protected]")
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

0 comments on commit 17e2627

Please sign in to comment.