Skip to content

Commit

Permalink
360Dialog Whatsapp Templates (#1090)
Browse files Browse the repository at this point in the history
* 1. Added APIs to create, delete and update whatsapp templates.
2. Added unit and integration test cases for the same.

* Fixed requested changes.

* Fixed requested changes.

---------

Co-authored-by: Nupur Khare <[email protected]>
  • Loading branch information
nupur-khare and Nupur Khare authored Dec 11, 2023
1 parent c58c168 commit bdfa6ad
Show file tree
Hide file tree
Showing 8 changed files with 707 additions and 8 deletions.
56 changes: 51 additions & 5 deletions kairon/api/app/routers/bot/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@
from starlette.requests import Request

from kairon import Utility
from kairon.events.definitions.message_broadcast import MessageBroadcastEvent
from kairon.shared.auth import Authentication
from kairon.api.models import (
Response,
Response, DictData,
)
from kairon.events.definitions.message_broadcast import MessageBroadcastEvent
from kairon.shared.auth import Authentication
from kairon.shared.channels.whatsapp.bsp.factory import BusinessServiceProviderFactory
from kairon.shared.chat.models import ChannelRequest, MessageBroadcastRequest
from kairon.shared.chat.broadcast.processor import MessageBroadcastProcessor
from kairon.shared.chat.models import ChannelRequest, MessageBroadcastRequest
from kairon.shared.chat.processor import ChatDataProcessor
from kairon.shared.constants import TESTER_ACCESS, DESIGNER_ACCESS, WhatsappBSPTypes, EventRequestType
from kairon.shared.models import User
from kairon.shared.data.processor import MongoProcessor
from kairon.shared.models import User

router = APIRouter()
mongo_processor = MongoProcessor()
Expand Down Expand Up @@ -107,6 +107,52 @@ async def initiate_platform_onboarding(
return Response(message='Channel added', data=channel_endpoint)


@router.post("/whatsapp/templates/{bsp_type}", response_model=Response)
async def add_message_templates(
request_data: DictData,
bsp_type: str = Path(default=None, description="Business service provider type",
example=WhatsappBSPTypes.bsp_360dialog.value),
current_user: User = Security(Authentication.get_current_user_and_bot, scopes=DESIGNER_ACCESS)
):
"""
Adds message templates for configured bsp account.
"""
provider = BusinessServiceProviderFactory.get_instance(bsp_type)(current_user.get_bot(), current_user.get_user())
response = provider.add_template(request_data.data, current_user.get_bot(), current_user.get_user())
return Response(data=response)


@router.put("/whatsapp/templates/{bsp_type}/{template_id}", response_model=Response)
async def edit_message_templates(
request_data: DictData,
template_id: str = Path(default=None, description="template id", example="594425479261596"),
bsp_type: str = Path(default=None, description="Business service provider type",
example=WhatsappBSPTypes.bsp_360dialog.value),
current_user: User = Security(Authentication.get_current_user_and_bot, scopes=DESIGNER_ACCESS)
):
"""
Edits message templates for configured bsp account.
"""
provider = BusinessServiceProviderFactory.get_instance(bsp_type)(current_user.get_bot(), current_user.get_user())
response = provider.edit_template(request_data.data, template_id),
return Response(data=response)


@router.delete("/whatsapp/templates/{bsp_type}/{template_id}", response_model=Response)
async def delete_message_templates(
template_id: str = Path(default=None, description="template id", example="594425479261596"),
bsp_type: str = Path(default=None, description="Business service provider type",
example=WhatsappBSPTypes.bsp_360dialog.value),
current_user: User = Security(Authentication.get_current_user_and_bot, scopes=DESIGNER_ACCESS)
):
"""
Deletes message templates for configured bsp account.
"""
provider = BusinessServiceProviderFactory.get_instance(bsp_type)(current_user.get_bot(), current_user.get_user())
response = provider.delete_template(template_id)
return Response(data=response)


@router.get("/whatsapp/templates/{bsp_type}/list", response_model=Response)
async def retrieve_message_templates(
request: Request,
Expand Down
12 changes: 12 additions & 0 deletions kairon/shared/channels/whatsapp/bsp/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ def post_process(self, **kwargs):
def save_channel_config(self, **kwargs):
raise NotImplementedError("Provider not implemented")

@abstractmethod
def add_template(self, **kwargs):
raise NotImplementedError("Provider not implemented")

@abstractmethod
def edit_template(self, **kwargs):
raise NotImplementedError("Provider not implemented")

@abstractmethod
def delete_template(self, **kwargs):
raise NotImplementedError("Provider not implemented")

@abstractmethod
def get_template(self, **kwargs):
raise NotImplementedError("Provider not implemented")
Expand Down
60 changes: 58 additions & 2 deletions kairon/shared/channels/whatsapp/bsp/dialog360.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import ast
from typing import Text
from typing import Text, Dict

from loguru import logger
from mongoengine import DoesNotExist

from kairon import Utility
from kairon.exceptions import AppException
from kairon.shared.account.activity_log import UserActivityLogger
from kairon.shared.channels.whatsapp.bsp.base import WhatsappBusinessServiceProviderBase
from kairon.shared.chat.processor import ChatDataProcessor
from kairon.shared.constants import WhatsappBSPTypes, ChannelTypes
from kairon.shared.constants import WhatsappBSPTypes, ChannelTypes, UserActivityType


class BSP360Dialog(WhatsappBusinessServiceProviderBase):
Expand Down Expand Up @@ -80,6 +82,60 @@ def save_channel_config(self, clientId: Text, client: Text, channels: list, part
}
return ChatDataProcessor.save_channel_config(conf, self.bot, self.user)

def add_template(self, data: Dict, bot: Text, user: Text):
try:
Utility.validate_create_template_request(data)
config = ChatDataProcessor.get_channel_config(ChannelTypes.WHATSAPP.value, self.bot, mask_characters=False)
partner_id = Utility.environment["channels"]["360dialog"]["partner_id"]
channel_id = config.get("config", {}).get("channel_id")
base_url = Utility.system_metadata["channels"]["whatsapp"]["business_providers"]["360dialog"]["hub_base_url"]
template_endpoint = f'/v1/partners/{partner_id}/waba_accounts/{channel_id}/waba_templates'
headers = {"Authorization": BSP360Dialog.get_partner_auth_token()}
url = f"{base_url}{template_endpoint}"
resp = Utility.execute_http_request(request_method="POST", http_url=url, request_body=data, headers=headers,
validate_status=True, err_msg="Failed to add template: ")
UserActivityLogger.add_log(a_type=UserActivityType.template_creation.value, email=user, bot=bot, message=['Template created!'])
return resp
except DoesNotExist as e:
logger.exception(e)
raise AppException("Channel not found!")

def edit_template(self, data: Dict, template_id: str):
try:
Utility.validate_edit_template_request(data)
config = ChatDataProcessor.get_channel_config(ChannelTypes.WHATSAPP.value, self.bot, mask_characters=False)
partner_id = Utility.environment["channels"]["360dialog"]["partner_id"]
channel_id = config.get("config", {}).get("channel_id")
base_url = Utility.system_metadata["channels"]["whatsapp"]["business_providers"]["360dialog"]["hub_base_url"]
template_endpoint = f'/v1/partners/{partner_id}/waba_accounts/{channel_id}/waba_templates/{template_id}'
headers = {"Authorization": BSP360Dialog.get_partner_auth_token()}
url = f"{base_url}{template_endpoint}"
resp = Utility.execute_http_request(request_method="PATCH", http_url=url, request_body=data, headers=headers,
validate_status=True, err_msg="Failed to edit template: ")
return resp
except DoesNotExist as e:
logger.exception(e)
raise AppException("Channel not found!")
except Exception as e:
logger.exception(e)
raise AppException(str(e))

def delete_template(self, template_id: str):
try:
config = ChatDataProcessor.get_channel_config(ChannelTypes.WHATSAPP.value, self.bot, mask_characters=False)
partner_id = Utility.environment["channels"]["360dialog"]["partner_id"]
channel_id = config.get("config", {}).get("channel_id")
base_url = Utility.system_metadata["channels"]["whatsapp"]["business_providers"]["360dialog"]["hub_base_url"]
template_endpoint = f'/v1/partners/{partner_id}/waba_accounts/{channel_id}/waba_templates/{template_id}'
headers = {"Authorization": BSP360Dialog.get_partner_auth_token()}
url = f"{base_url}{template_endpoint}"
resp = Utility.execute_http_request(request_method="DELETE", http_url=url, headers=headers,
validate_status=True, err_msg="Failed to delete template: ")
return resp
except DoesNotExist as e:
logger.exception(e)
raise AppException("Channel not found!")

def get_template(self, template_id: Text):
return self.list_templates(id=template_id)

Expand Down
1 change: 1 addition & 0 deletions kairon/shared/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class UserActivityType(str, Enum):
login = 'login'
login_refresh_token = "login_refresh_token"
invalid_login = 'invalid_login'
template_creation = 'template_creation'


class EventClass(str, Enum):
Expand Down
15 changes: 14 additions & 1 deletion kairon/shared/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1065,6 +1065,19 @@ def reload_model(bot: Text, email: Text):
else:
raise AppException("Agent config not found!")

@staticmethod
def validate_create_template_request(data: Dict):
required_keys = ['name', 'category', 'components', 'language']
missing_keys = [key for key in required_keys if key not in data]
if missing_keys:
raise AppException(f'Missing {", ".join(missing_keys)} in request body!')

@staticmethod
def validate_edit_template_request(data: Dict):
non_editable_keys = ['name', 'category', 'language']
if any(key in data for key in non_editable_keys):
raise AppException('Only "components" and "allow_category_change" fields can be edited!')

@staticmethod
def initiate_fastapi_apm_client():
from elasticapm.contrib.starlette import make_apm_client
Expand Down Expand Up @@ -1402,7 +1415,7 @@ def execute_http_request(
response = requests.request(
request_method.upper(), http_url, params=request_body, headers=headers, timeout=kwargs.get('timeout')
)
elif request_method.lower() in ['post', 'put']:
elif request_method.lower() in ['post', 'put', 'patch']:
response = session.request(
request_method.upper(), http_url, json=request_body, headers=headers, timeout=kwargs.get('timeout')
)
Expand Down
1 change: 1 addition & 0 deletions tests/integration_test/chat_service_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1341,6 +1341,7 @@ def _mock_validate_hub_signature(*args, **kwargs):

@responses.activate
def test_whatsapp_valid_order_message_request():
responses.reset()
def _mock_validate_hub_signature(*args, **kwargs):
return True

Expand Down
Loading

0 comments on commit bdfa6ad

Please sign in to comment.