diff --git a/api/blueprints/message/chat/list.py b/api/blueprints/message/chat/list.py index dab5c38..2ee5383 100644 --- a/api/blueprints/message/chat/list.py +++ b/api/blueprints/message/chat/list.py @@ -6,11 +6,11 @@ DATE: - Thursday, 19th Dec., 2024 + Tuesday, 21st Jan., 2025. OBJECTIVE: - To list SMS messages associated with incoming identifiers. + To list chat messages associated with incoming identifiers. REFERENCES: @@ -64,7 +64,7 @@ from shared import constants # Data Models: from models.core.user import CoreUserInfoModel -from models.api.message.sms.list import SMSListRequestHeaders, SMSListRequestData +from models.api.message.chat.list import ChatMessageListRequestHeaders, ChatMessageListRequestData # Helpers: from api.helpers.user import token_check @@ -84,7 +84,7 @@ import asyncio # Related to Quart: -sms_list_bp = Blueprint("sms_list", __name__) +chat_list_bp = Blueprint("chat_list", __name__) # ***************************************************************************************************************** @@ -104,7 +104,7 @@ sms_list_bp = Blueprint("sms_list", __name__) # ***************************************************************************************************************** -@sms_list_bp.record_once +@chat_list_bp.record_once def init(blueprint_setup_state): # This gets called when the blueprint is registered. @@ -115,7 +115,7 @@ def init(blueprint_setup_state): # --------------------------------------------------------------------------------------------------------------------- -@sms_list_bp.route("/list", methods = ["GET"]) +@chat_list_bp.route("/list", methods = ["GET"]) @set_api_version(api_version = "1.0.0") @read_input(sanitize_headers = False, sanitize_data = False) @get_session_info(key = "X-Session-Token", session_coro = "get_session") @@ -123,7 +123,7 @@ def init(blueprint_setup_state): attr_name = "logs_mongo", project = constants.PROJECT_NAME, log_type = constants.MODULE_NAME, - operation = "smsListApi", + operation = "chatMsgListApi", log_input = True, log_output = True, sensitive_keys = ["sessionToken", "X-Session-Token", "tokenKeys"] @@ -131,13 +131,13 @@ def init(blueprint_setup_state): @log_chain_to_mongo(attr_name = "logs_mongo") @should_not_be_under_maintenance(attr_name = "is_under_maintenance") @validate_input( - header_validator = lambda x: SMSListRequestHeaders(**x).model_dump(), - data_validator = lambda x: SMSListRequestData(**x) + header_validator = lambda x: ChatMessageListRequestHeaders(**x).model_dump(), + data_validator = lambda x: ChatMessageListRequestData(**x) ) @handle_cancelled_request() -async def list_sms_messages( - inbound_headers: dict | SMSListRequestHeaders = None, - inbound_data: dict | SMSListRequestData = None, +async def list_chat_messages( + inbound_headers: dict | ChatMessageListRequestHeaders = None, + inbound_data: dict | ChatMessageListRequestData = None, inbound_files: dict = None, **kwargs ): @@ -168,7 +168,7 @@ async def list_sms_messages( # ┛ # Get the tokens from the database: - auth_tokens = await current_app.sms_controller.get_tokens_from_keys( + auth_tokens = await current_app.chat_controller.get_tokens_from_keys( mongo_data_conn = current_app.data_mongo, token_keys = inbound_data.tokenKeys, limit = len(inbound_data.tokenKeys) @@ -177,13 +177,13 @@ async def list_sms_messages( # Check if these tokens belong to the user claiming ownership: if not await token_check.is_authorized( - mongo_conn = current_app.data_mongo, + mongo_data_conn = current_app.data_mongo, user_info = CoreUserInfoModel(**kwargs["session_info"]), token_ids = token_ids ): return ResponseModel( status_code = StatusCodes.FAILED, http_code = HttpCodes.UNAUTHORIZED, - message = "User doesn't have rights over one or more SMS accounts." + message = "User doesn't have rights over one or more chat accounts." ) # ┳┓ ┓ • • @@ -191,15 +191,27 @@ async def list_sms_messages( # ┻┛┗┻┗┗┻ ┗┛┗┛┗┗┛┗┗┫ # ┛ - messages = await current_app.sms_controller.get_messages( + # Construct any additional filter: + additional_filter = {} + if inbound_data.tags: additional_filter["tags"] = {"$in": inbound_data.tags} + + # Fetch the messages: + messages = await current_app.chat_controller.get_messages( mongo_data_conn = current_app.data_mongo, token_ids = token_ids, limit = inbound_data.count, skip = inbound_data.fromCount, projection = { - "message.metadata": False, - "message.rawResponse": False - } + "message.httpCode": False, + "message.status": False, + "message.statusCode": False, + "message.apiMessage": False, + "message.requestId": False, + "message.messageCount": False, + "message.messageCost": False, + "message.balance": False, + }, + additional_filter = additional_filter ) # ┳┓ @@ -214,7 +226,7 @@ async def list_sms_messages( status_code = StatusCodes.OK if success else StatusCodes.FAILED, http_code = HttpCodes.SUCCESS if success else HttpCodes.NOT_FOUND, data = [m.full for m in messages] if success else None, - message = f"{message_count} SMS message(s) found." + message = f"{message_count} message(s) found." ) diff --git a/api/blueprints/message/chat/send.py b/api/blueprints/message/chat/send.py index 12e550c..6040bd7 100644 --- a/api/blueprints/message/chat/send.py +++ b/api/blueprints/message/chat/send.py @@ -6,11 +6,11 @@ DATE: - Thursday, 19th Dec., 2024 + Tuesday, 21st Jan., 2025. OBJECTIVE: - To send SMS messages through various third-party clients. + To send chat-app messages through various third-party clients. REFERENCES: @@ -62,12 +62,8 @@ from utils_v2.api.async_quart import ( # Models: from models.core.auth_token import CoreAuthTokenModel -from models.api.message.sms.send import SMSSendRequestHeaders, SMSSendRequestData -from models.message.sms.send import ( - NimbusSMSIndiaMessage, - SavvyBulkSMSKenyaMessage, - SMSSendManyResults -) +from models.api.message.chat.send import ChatSendRequestHeaders, ChatSendRequestData +from models.message.chat.send import ChatSendOneResult, ChatSendManyResults, NimbusWhatsAppMessage # Common: from shared import constants @@ -89,7 +85,7 @@ import asyncio # Related to Quart: -sms_send_bp = Blueprint("sms_send", __name__) +chat_send_bp = Blueprint("chat_send", __name__) # ***************************************************************************************************************** @@ -109,7 +105,7 @@ sms_send_bp = Blueprint("sms_send", __name__) # ***************************************************************************************************************** -@sms_send_bp.record_once +@chat_send_bp.record_once def init(blueprint_setup_state): # This gets called when the blueprint is registered. @@ -120,44 +116,43 @@ def init(blueprint_setup_state): # --------------------------------------------------------------------------------------------------------------------- -async def send_sms_messages( - mongo_data_conn: AsyncMongo, +async def send_chat_messages( auth_token: CoreAuthTokenModel, - messages: List[NimbusSMSIndiaMessage | SavvyBulkSMSKenyaMessage], + messages: List[NimbusWhatsAppMessage], tags: List[Any] -) -> SMSSendManyResults: +) -> ChatSendManyResults: """ This function purely tackles message sending. It is not concerned with authorization and security checks. Please ensure that you perform those checks before coming here. - :param mongo_data_conn: The database connection to use to perform this task. :param auth_token: The auth token that will be used to send this message. :param messages: The list of messages to send out. - :param tags: Any tags to attach with these SMS for filtering when querying in the listing service. - :return: The structured result of sending many SMS messages. + :param tags: Any tags to attach with these messages for filtering when querying in the listing service. + :return: The structured result of sending many chat messages. """ # Start by assuming failure: - results = SMSSendManyResults() + client_controller = None + results = ChatSendManyResults() # Select the right client: match auth_token.client: - case "nimbusSmsIndia": - results = await current_app.nimbus_sms_india_controller.send_many_sms( - mongo_data_conn = mongo_data_conn, - auth_token = auth_token, - messages = messages, - tags = tags - ) - case "savvyBulkSmsKenya": - results = await current_app.savvy_bulk_sms_kenya_controller.send_many_sms( - mongo_data_conn = mongo_data_conn, - auth_token = auth_token, - messages = messages, - tags = tags - ) + case "whatsappNimbus": client_controller = current_app.whatsapp_nimbus_controller case _: - results.message = "Invalid/unimplemented SMS client." + client_controller = None + results.message = f"Invalid/unimplemented client '{auth_token.client}'." + + # If we have a match: + if client_controller is not None: + results = await client_controller.send_many_messages( + sql_conn = current_app.sql_writer, + mongo_data_conn = current_app.data_mongo, + http_client = current_app.http_client, + auth_token = auth_token, + client = None, + messages = messages, + tags = tags + ) # Done here: return results @@ -166,8 +161,8 @@ async def send_sms_messages( # --------------------------------------------------------------------------------------------------------------------- -@sms_send_bp.route("", methods = ["POST"]) -@sms_send_bp.route("/send", methods = ["POST"]) +@chat_send_bp.route("", methods = ["POST"]) +@chat_send_bp.route("/send", methods = ["POST"]) @set_api_version(api_version = "1.0.0") @read_input(sanitize_headers = False, sanitize_data = False) @get_session_info(key = "X-Session-Token", session_coro = "get_session") @@ -175,27 +170,27 @@ async def send_sms_messages( attr_name = "logs_mongo", project = constants.PROJECT_NAME, log_type = constants.MODULE_NAME, - operation = "smsSendApi", - log_input = True, + operation = "chatSendApi", + log_input = 1, log_output = True, sensitive_keys = ["sessionToken", "X-Session-Token", "tokenKey"] ) @log_chain_to_mongo(attr_name = "logs_mongo") @should_not_be_under_maintenance(attr_name = "is_under_maintenance") @validate_input( - header_validator = lambda x: SMSSendRequestHeaders(**x).model_dump(), - data_validator = lambda x: SMSSendRequestData(**x) + header_validator = lambda x: ChatSendRequestHeaders(**x).model_dump(), + data_validator = lambda x: ChatSendRequestData(**x) ) @handle_cancelled_request() -async def send_sms_messages_api( - inbound_headers: dict | SMSSendRequestHeaders = None, - inbound_data: dict | SMSSendRequestData = None, +async def send_chat_messages_api( + inbound_headers: dict | ChatSendRequestHeaders = None, + inbound_data: dict | ChatSendRequestData = None, inbound_files: dict = None, **kwargs ): """ - Use this API when someone wants to send one or more SMS messages. + Use this API when someone wants to send one or more chat messages. :param inbound_headers: auto-extracted by the decorators. :param inbound_data: auto-extracted by the decorators. :param inbound_files: auto-extracted by the decorators. @@ -220,7 +215,7 @@ async def send_sms_messages_api( ) # Get the token from the token key: - auth_token = await current_app.sms_controller.get_token_from_key( + auth_token = await current_app.chat_controller.get_token_from_key( mongo_data_conn = current_app.data_mongo, token_key = inbound_data.tokenKey ) @@ -230,12 +225,12 @@ async def send_sms_messages_api( message = f"No such token key." ) - # ┏┓ ┓ ┏┳┓┓ ┏┓┳┳┓┏┓ - # ┗┓┏┓┏┓┏┫ ┃ ┣┓┏┓ ┗┓┃┃┃┗┓ - # ┗┛┗ ┛┗┗┻ ┻ ┛┗┗ ┗┛┛ ┗┗┛ + # ┏┓ ┓ ┓ ┳┳┓ + # ┗┓┏┓┏┓┏┫ ╋┣┓┏┓ ┃┃┃┏┓┏┏┏┓┏┓┏┓ + # ┗┛┗ ┛┗┗┻ ┗┛┗┗ ┛ ┗┗ ┛┛┗┻┗┫┗ + # ┛ - sending_results = await send_sms_messages( - mongo_data_conn = current_app.data_mongo, + sending_results = await send_chat_messages( auth_token = auth_token, messages = inbound_data.message, tags = inbound_data.tags diff --git a/api/blueprints/message/chat/tags.py b/api/blueprints/message/chat/tags.py index 803418b..7017f4f 100644 --- a/api/blueprints/message/chat/tags.py +++ b/api/blueprints/message/chat/tags.py @@ -6,11 +6,11 @@ DATE: - Thursday, 19th Dec., 2024 + Tuesday, 21st Jan., 2025. OBJECTIVE: - To update tags on SMS messages. + To update tags on chat messages. REFERENCES: @@ -64,7 +64,7 @@ from shared import constants # Data Models: from models.core.user import CoreUserInfoModel -from models.api.message.sms.tags import SMSUpdateTagsRequestHeaders, SMSUpdateTagsRequestData +from models.api.message.chat.tags import ChatMessageUpdateTagsRequestHeaders, ChatMessageUpdateTagsRequestData # Helpers: from api.helpers.user import token_check @@ -84,7 +84,7 @@ import asyncio # Related to Quart: -sms_update_tags_bp = Blueprint("sms_upd_tags", __name__) +chat_update_tags_bp = Blueprint("chat_upd_tags", __name__) # ***************************************************************************************************************** @@ -104,7 +104,7 @@ sms_update_tags_bp = Blueprint("sms_upd_tags", __name__) # ***************************************************************************************************************** -@sms_update_tags_bp.record_once +@chat_update_tags_bp.record_once def init(blueprint_setup_state): # This gets called when the blueprint is registered. @@ -115,7 +115,7 @@ def init(blueprint_setup_state): # --------------------------------------------------------------------------------------------------------------------- -@sms_update_tags_bp.route("/tags", methods = ["PATCH"]) +@chat_update_tags_bp.route("/tags", methods = ["PATCH"]) @set_api_version(api_version = "1.0.0") @read_input(sanitize_headers = False, sanitize_data = False) @get_session_info(key = "X-Session-Token", session_coro = "get_session") @@ -123,7 +123,7 @@ def init(blueprint_setup_state): attr_name = "logs_mongo", project = constants.PROJECT_NAME, log_type = constants.MODULE_NAME, - operation = "smsUpdTagsApi", + operation = "chatUpdTagsApi", log_input = True, log_output = True, sensitive_keys = ["sessionToken", "X-Session-Token", "tokenKey"] @@ -131,13 +131,13 @@ def init(blueprint_setup_state): @log_chain_to_mongo(attr_name = "logs_mongo") @should_not_be_under_maintenance(attr_name = "is_under_maintenance") @validate_input( - header_validator = lambda x: SMSUpdateTagsRequestHeaders(**x).model_dump(), - data_validator = lambda x: SMSUpdateTagsRequestData(**x) + header_validator = lambda x: ChatMessageUpdateTagsRequestHeaders(**x).model_dump(), + data_validator = lambda x: ChatMessageUpdateTagsRequestData(**x) ) @handle_cancelled_request() -async def update_sms_tags( - inbound_headers: dict | SMSUpdateTagsRequestHeaders = None, - inbound_data: dict | SMSUpdateTagsRequestData = None, +async def update_chat_message_tags( + inbound_headers: dict | ChatMessageUpdateTagsRequestHeaders = None, + inbound_data: dict | ChatMessageUpdateTagsRequestData = None, inbound_files: dict = None, **kwargs ): @@ -168,7 +168,7 @@ async def update_sms_tags( # ┛ # Get the message: - message = await current_app.sms_controller.get_message( + message = await current_app.chat_controller.get_message( mongo_data_conn = current_app.data_mongo, message_id = inbound_data.messageId ) @@ -195,7 +195,7 @@ async def update_sms_tags( # ┛ ┛ # Update the message: - success = await current_app.sms_controller.update_sms_tags( + success = await current_app.chat_controller.update_chat_message_tags( mongo_data_conn = current_app.data_mongo, message_id = inbound_data.messageId, unset_tags = inbound_data.unsetTags, diff --git a/api/main.py b/api/main.py index da3766a..b57ebfd 100644 --- a/api/main.py +++ b/api/main.py @@ -62,14 +62,17 @@ from utils_v2.api.async_quart import ( # GMail-related utils: from utils_v2.goog.controllers.gmail.gmail_client import AsyncGmailClient +# Chat clients: +from utils_v2.whatsapp.nimbus.controllers.async_nimbus_whatsapp import AsyncNimbusWhatsapp + # Core Controller Models: from controllers.core.message import CoreMessageController # from controllers.core.auth_token import CoreAuthTokenController from controllers.core.ai.llm import CoreLLMController from controllers.core.payment import CorePaymentController -# API Controller Models: -from controllers.api.mail import MailController +# # API Controller Models: +# from controllers.api.mail import MailController # from controllers.api.sms import SMSController # from controllers.api.payment import PaymentController @@ -118,6 +121,9 @@ from api.blueprints.message.sms.tags import sms_update_tags_bp # Chat Blueprints: from api.blueprints.message.chat.auth import chat_auth_bp # from api.blueprints.message.chat.webhook import chat_webhook_bp +from api.blueprints.message.chat.send import chat_send_bp +from api.blueprints.message.chat.list import chat_list_bp +from api.blueprints.message.chat.tags import chat_update_tags_bp # Software Blueprints: from api.blueprints.software.auth import sw_auth_bp @@ -187,6 +193,9 @@ app.register_blueprint(sms_update_tags_bp, url_prefix = f"/{MODULE_BASE}/sms") # Chat Blueprints: app.register_blueprint(chat_auth_bp, url_prefix = f"/{MODULE_BASE}/chat") # app.register_blueprint(chat_webhook_bp, url_prefix = f"/{MODULE_BASE}/chat") +app.register_blueprint(chat_send_bp, url_prefix = f"/{MODULE_BASE}/chat") +app.register_blueprint(chat_list_bp, url_prefix = f"/{MODULE_BASE}/chat") +app.register_blueprint(chat_update_tags_bp, url_prefix = f"/{MODULE_BASE}/chat") # Software Blueprints: app.register_blueprint(sw_auth_bp, url_prefix = f"/{MODULE_BASE}/software") diff --git a/controllers_v2/message/chat/all_chat.py b/controllers_v2/message/chat/all_chat.py index fb7d2a3..663c5bb 100644 --- a/controllers_v2/message/chat/all_chat.py +++ b/controllers_v2/message/chat/all_chat.py @@ -36,19 +36,24 @@ sys.path.append(".") sys.path.append("..") # My async utils: -from utils_v2.date_time import date_time +from utils_v2.database.async_mysql_v2 import AsyncMySQL from utils_v2.database.async_mongo_v2 import AsyncMongo from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache # Controllers: +from controllers_v2.core.message import CoreMessageController from controllers_v2.message.chat.base import ChatController # Models: from models.core.auth_token import CoreAuthTokenModel -from models.core.message import CoreMessageModel +from models.message.chat.send import ( + NimbusWhatsAppMessage, + ChatSendOneResult, + ChatSendManyResults +) -# SMS Clients: -from utils_v2.sms.india.nimbus.controllers.async_nimbus import AsyncNimbusSMS +# Chat clients: +from utils_v2.whatsapp.nimbus.controllers.async_nimbus_whatsapp import AsyncNimbusWhatsapp # To work with datatypes: from typing import List, Any @@ -56,8 +61,11 @@ from typing import List, Any # To make HTTP requests: import httpx -# For asynchronous activities: -import asyncio +# to work with MongoDB: +from bson.objectid import ObjectId + +# To make abstract classes: +from abc import ABC, abstractmethod # ***************************************************************************************************************** @@ -128,12 +136,65 @@ class AllChatController(ChatController): cache = cache, alert_url = alert_url, http_client = http_client, - base_filter = {"client": "whatsappNimbus"}, + base_filter = {}, debug = debug, debug_prefix = debug_prefix, debug_only_errors = debug_only_errors ) + # ┏┓ ┓ ┳┳┓ + # ┗┓┏┓┏┓┏┫ ┃┃┃┏┓┏┏┏┓┏┓┏┓┏ + # ┗┛┗ ┛┗┗┻ ┛ ┗┗ ┛┛┗┻┗┫┗ ┛ + # ┛ + + async def send_one_message( + self, + sql_conn: AsyncMySQL, + mongo_data_conn: AsyncMongo, + auth_token: CoreAuthTokenModel, + client: AsyncNimbusWhatsapp, + message: NimbusWhatsAppMessage, + tags: List[Any] + ) -> ChatSendOneResult: + + """ + To send one message from the third-party client. + :param sql_conn: The connection to the database to use for this operation. + :param mongo_data_conn: The connection to the database to use for this operation. + :param auth_token: The auth-token model for the account from which the message has to be sent. + :param client: The connection/instance of the third-party client to use to perform this operation. + :param message: The message that you want to send to the recipient. + :param tags: Any tags that you would like to attach to the message. To be used later for internal filtering. + :return: The structured response model to describe the operation. + """ + + raise NotImplementedError + + async def send_many_messages( + self, + sql_conn: AsyncMySQL, + mongo_data_conn: AsyncMongo, + http_client: httpx.AsyncClient, + auth_token: CoreAuthTokenModel, + client: AsyncNimbusWhatsapp | None, + messages: List[NimbusWhatsAppMessage], + tags: List[Any] + ) -> ChatSendManyResults: + + """ + To send many chat messages in one go. + :param sql_conn: The connection to the database to use for this operation. + :param mongo_data_conn: The connection to the database to use for this operation. + :param http_client: An HTTP client to use to make API calls through the third-party client's class. + :param auth_token: The auth-token model for the account from which the message has to be sent. + :param client: The connection/instance of the third-party client to use to perform this operation. + :param messages: The messages that you want to send to the recipients. + :param tags: Any tags that you would like to attach to the message. To be used later for internal filtering. + :return: The structured response model to describe the operation. + """ + + raise NotImplementedError + # ***************************************************************************************************************** # ***** **** diff --git a/controllers_v2/message/chat/base.py b/controllers_v2/message/chat/base.py index e97e909..b11040f 100644 --- a/controllers_v2/message/chat/base.py +++ b/controllers_v2/message/chat/base.py @@ -36,6 +36,7 @@ sys.path.append(".") sys.path.append("..") # My async utils: +from utils_v2.database.async_mysql_v2 import AsyncMySQL from utils_v2.database.async_mongo_v2 import AsyncMongo from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache @@ -44,16 +45,14 @@ from controllers_v2.core.message import CoreMessageController # Models: from models.core.auth_token import CoreAuthTokenModel -from models.api.message.sms.send import ( - NimbusSMSIndiaMessage, - SavvyBulkSMSKenyaMessage, - SMSSendOneResult, - SMSSendManyResults +from models.message.chat.send import ( + NimbusWhatsAppMessage, + ChatSendOneResult, + ChatSendManyResults ) -# SMS clients: -from utils_v2.sms.india.nimbus.controllers.async_nimbus import AsyncNimbusSMS -from utils_v2.sms.kenya.savvy_bulk_sms.controllers.async_savvy_bulk_sms import AsyncSavvyBulkSMS +# Chat clients: +from utils_v2.whatsapp.nimbus.controllers.async_nimbus_whatsapp import AsyncNimbusWhatsapp # To work with datatypes: from typing import List, Any @@ -61,6 +60,9 @@ from typing import List, Any # To make HTTP requests: import httpx +# to work with MongoDB: +from bson.objectid import ObjectId + # To make abstract classes: from abc import ABC, abstractmethod @@ -135,7 +137,7 @@ class ChatController(CoreMessageController, ABC): # Prepare the combined base filter: sms_filter = {} for k, v in (base_filter or {}).items(): sms_filter[k] = v - sms_filter["serviceType"] = "sms" + sms_filter["serviceType"] = "chat" # Invoke the parent's constructor: CoreMessageController.__init__( @@ -149,6 +151,94 @@ class ChatController(CoreMessageController, ABC): debug_only_errors = debug_only_errors ) + # ┏┓ ┓ ┳┳┓ + # ┗┓┏┓┏┓┏┫ ┃┃┃┏┓┏┏┏┓┏┓┏┓┏ + # ┗┛┗ ┛┗┗┻ ┛ ┗┗ ┛┛┗┻┗┫┗ ┛ + # ┛ + + @abstractmethod + async def send_one_message( + self, + sql_conn: AsyncMySQL, + mongo_data_conn: AsyncMongo, + auth_token: CoreAuthTokenModel, + client: AsyncNimbusWhatsapp, + message: NimbusWhatsAppMessage, + tags: List[Any] + ) -> ChatSendOneResult: + + """ + To send one message from the third-party client. + :param sql_conn: The connection to the database to use for this operation. + :param mongo_data_conn: The connection to the database to use for this operation. + :param auth_token: The auth-token model for the account from which the message has to be sent. + :param client: The connection/instance of the third-party client to use to perform this operation. + :param message: The message that you want to send to the recipient. + :param tags: Any tags that you would like to attach to the message. To be used later for internal filtering. + :return: The structured response model to describe the operation. + """ + + pass + + @abstractmethod + async def send_many_messages( + self, + sql_conn: AsyncMySQL, + mongo_data_conn: AsyncMongo, + http_client: httpx.AsyncClient, + auth_token: CoreAuthTokenModel, + client: AsyncNimbusWhatsapp | None, + messages: List[NimbusWhatsAppMessage], + tags: List[Any] + ) -> ChatSendManyResults: + + """ + To send many chat messages in one go. + :param sql_conn: The connection to the database to use for this operation. + :param mongo_data_conn: The connection to the database to use for this operation. + :param http_client: An HTTP client to use to make API calls through the third-party client's class. + :param auth_token: The auth-token model for the account from which the message has to be sent. + :param client: The connection/instance of the third-party client to use to perform this operation. + :param messages: The messages that you want to send to the recipients. + :param tags: Any tags that you would like to attach to the message. To be used later for internal filtering. + :return: The structured response model to describe the operation. + """ + + pass + + # ┳┳ ┓ ┏┳┓ + # ┃┃┏┓┏┫┏┓╋┏┓ ┃ ┏┓┏┓┏ + # ┗┛┣┛┗┻┗┻┗┗ ┻ ┗┻┗┫┛ + # ┛ ┛ + + # We cannot modify the SMS messages themselves, but we can set/unset tags on them for internal referencing and + # filtering. This will help the users organize their inboxes well. + + async def update_chat_message_tags( + self, + mongo_data_conn: AsyncMongo, + message_id: ObjectId | str, + unset_tags: List[str] = None, + set_tags: List[str] = None + ) -> bool: + + """ + To set and unset tags on one chat message. + :param mongo_data_conn: The database connection to use to perform this action. + :param message_id: The ObjectId of the document in MongoDb that holds the message. + :param unset_tags: The list of tags to unset (done before setting new tags). + :param set_tags: The list of tags to set (done after unsetting old tags). + :return: True if successful, else False. + """ + + # Simply call the core model: + return await self.update_message_tags( + mongo_data_conn = mongo_data_conn, + message_id = message_id, + unset_tags = unset_tags, + set_tags = set_tags + ) + # ***************************************************************************************************************** # ***** **** diff --git a/controllers_v2/message/chat/whatsapp_nimbus.py b/controllers_v2/message/chat/whatsapp_nimbus.py index a87641a..f6d1938 100644 --- a/controllers_v2/message/chat/whatsapp_nimbus.py +++ b/controllers_v2/message/chat/whatsapp_nimbus.py @@ -37,6 +37,7 @@ sys.path.append("..") # My async utils: from utils_v2.date_time import date_time +from utils_v2.database.async_mysql_v2 import AsyncMySQL from utils_v2.database.async_mongo_v2 import AsyncMongo from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache @@ -46,9 +47,14 @@ from controllers_v2.message.chat.base import ChatController # Models: from models.core.auth_token import CoreAuthTokenModel from models.core.message import CoreMessageModel +from models.message.chat.send import ( + NimbusWhatsAppMessage, + ChatSendOneResult, + ChatSendManyResults +) -# SMS Clients: -from utils_v2.sms.india.nimbus.controllers.async_nimbus import AsyncNimbusSMS +# Chat clients: +from utils_v2.whatsapp.nimbus.controllers.async_nimbus_whatsapp import AsyncNimbusWhatsapp # To work with datatypes: from typing import List, Any @@ -134,6 +140,136 @@ class WhatsAppNimbusController(ChatController): debug_only_errors = debug_only_errors ) + # ┏┓ ┓ ┳┳┓ + # ┗┓┏┓┏┓┏┫ ┃┃┃┏┓┏┏┏┓┏┓┏┓┏ + # ┗┛┗ ┛┗┗┻ ┛ ┗┗ ┛┛┗┻┗┫┗ ┛ + # ┛ + + async def send_one_message( + self, + sql_conn: AsyncMySQL, + mongo_data_conn: AsyncMongo, + auth_token: CoreAuthTokenModel, + client: AsyncNimbusWhatsapp, + message: NimbusWhatsAppMessage, + tags: List[Any] + ) -> ChatSendOneResult: + + """ + To send one message from the third-party client. + :param sql_conn: The connection to the database to use for this operation. + :param mongo_data_conn: The connection to the database to use for this operation. + :param auth_token: The auth-token model for the account from which the message has to be sent. + :param client: The connection/instance of the third-party client to use to perform this operation. + :param message: The message that you want to send to the recipient. + :param tags: Any tags that you would like to attach to the message. To be used later for internal filtering. + :return: The structured response model to describe the operation. + """ + + # Send the SMS: + client_response = await client.send_whatsapp( + recipient_number = message.recipientNo, + message = message.message, + pdf_url = message.pdfUrl, + image_0_url = message.image0Url, + image_1_url = message.image1Url, + schedule_on = message.scheduleTs + ) + + # Convert the format of the SMS client's response to the core message model. + sent_message_model = CoreMessageModel( + ts = client_response.ts, + syncTs = date_time.get_current_utc_date_time(as_string = False), + tokenId = auth_token.authTokenId, + serviceType = auth_token.serviceType, + client = auth_token.client, + clientMessageId = client_response.requestId, + clientThreadId = message.recipientNo, + isSent = True, + isBroadcast = False, + sentSuccessfully = client_response.success, + sender = auth_token.clientUserId["senderId"], + recipient = message.recipientNo, + chat = message.recipientNo, + message = client_response.model_dump(), + snippet = message.message, + aiSnippet = None, + tags = list(set(tags + ["Chat", "Nimbus", "WhatsApp"])) + ) + + # Save the result to the database: + message_id = await self.save_one_message( + mongo_data_conn = mongo_data_conn, + message = sent_message_model + ) + self._printer(message_id, client_response.success) + + # Done here: + success = True if client_response.success and message_id else False + return ChatSendOneResult( + success = success, + message = "Message sent successfully." if client_response.success else "Message sending failed.", + chatMessage = sent_message_model + ) + + async def send_many_messages( + self, + sql_conn: AsyncMySQL, + mongo_data_conn: AsyncMongo, + http_client: httpx.AsyncClient, + auth_token: CoreAuthTokenModel, + client: AsyncNimbusWhatsapp | None, + messages: List[NimbusWhatsAppMessage], + tags: List[Any] + ) -> ChatSendManyResults: + + """ + To send many chat messages in one go. + :param sql_conn: The connection to the database to use for this operation. + :param mongo_data_conn: The connection to the database to use for this operation. + :param http_client: An HTTP client to use to make API calls through the third-party client's class. + :param auth_token: The auth-token model for the account from which the message has to be sent. + :param client: The connection/instance of the third-party client to use to perform this operation. + :param messages: The messages that you want to send to the recipients. + :param tags: Any tags that you would like to attach to the message. To be used later for internal filtering. + :return: The structured response model to describe the operation. + """ + + # Start with a blank variable: + cumulative_results = ChatSendManyResults() + + # Make the client from the auth-token: + client = client or AsyncNimbusWhatsapp( + api_key = auth_token.auth["apiKey"], + http_client = self._http_client, + debug = False + ) + + # Create and fire all the message-sending tasks: + tasks = [ + self.send_one_message( + sql_conn = sql_conn, + mongo_data_conn = mongo_data_conn, + auth_token = auth_token, + client = client, + message = message, + tags = tags + ) + for message in messages + ] + individual_results = await asyncio.gather(*tasks) + + # Prepare the final result: + for result in individual_results: + if result.success: cumulative_results.successCount += 1 + else: cumulative_results.failureCount += 1 + cumulative_results.totalCount += 1 + cumulative_results.chatMessages.append(result.chatMessage) + cumulative_results.message = f"{cumulative_results.successCount}/{cumulative_results.totalCount} messgae(s) sent." + + # Done here: + return cumulative_results + # ***************************************************************************************************************** # ***** **** diff --git a/controllers_v2/message/sms/base.py b/controllers_v2/message/sms/base.py index a9e07fe..dedacfd 100644 --- a/controllers_v2/message/sms/base.py +++ b/controllers_v2/message/sms/base.py @@ -44,7 +44,7 @@ from controllers_v2.core.message import CoreMessageController # Models: from models.core.auth_token import CoreAuthTokenModel -from models.api.message.sms.send import ( +from models.message.sms.send import ( NimbusSMSIndiaMessage, SavvyBulkSMSKenyaMessage, SMSSendOneResult, @@ -157,12 +157,13 @@ class SMSController(CoreMessageController, ABC): # ┗┛┛ ┗┗┛ ┗┛┗ ┛┗┗┻┗┛┗┗┫ # ┛ + @abstractmethod async def send_one_sms( self, mongo_data_conn: AsyncMongo, auth_token: CoreAuthTokenModel, client: AsyncNimbusSMS | AsyncSavvyBulkSMS, - message: NimbusSMSIndiaMessage, + message: NimbusSMSIndiaMessage | SavvyBulkSMSKenyaMessage, tags: List[Any] ) -> SMSSendOneResult: diff --git a/controllers_v2/message/sms/nimbus_sms_india.py b/controllers_v2/message/sms/nimbus_sms_india.py index ad213e4..b2552af 100644 --- a/controllers_v2/message/sms/nimbus_sms_india.py +++ b/controllers_v2/message/sms/nimbus_sms_india.py @@ -184,7 +184,7 @@ class NimbusSMSIndiaController(SMSController): isSent = True, isBroadcast = False, sentSuccessfully = client_response.success, - sender = auth_token.auth["senderId"], + sender = auth_token.clientUserId["senderId"], recipient = message.recipientNo, chat = message.recipientNo, message = client_response.model_dump(), diff --git a/models/api/message/chat/list.py b/models/api/message/chat/list.py index 04bd4a5..c087d6c 100644 --- a/models/api/message/chat/list.py +++ b/models/api/message/chat/list.py @@ -6,11 +6,11 @@ DATE: - Tuesday, 3rd Dec., 2024. + Tuesday, 21st Jan., 2025. OBJECTIVE: - To provide a structure to query the full payload of an email. + To provide a structure to query the full payload of chat message(s). REFERENCES: @@ -75,10 +75,10 @@ REGEX_SESSION_TOKEN = r"^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9] # ***************************************************************************************************************** -class SMSListRequestHeaders(BaseModel): +class ChatMessageListRequestHeaders(BaseModel): sessionToken: str = Field( - description = "the session token of the user who is requesting the service", + description = "The session token of the user who is requesting the service.", pattern = REGEX_SESSION_TOKEN, frozen = True, alias = "X-Session-Token" @@ -99,10 +99,10 @@ class SMSListRequestHeaders(BaseModel): # --------------------------------------------------------------------------------------------------------------------- -class SMSListRequestData(BaseModel): +class ChatMessageListRequestData(BaseModel): tokenKeys: str | List[str] = Field( - description = "the token identifier(s) that tell you which auth-tokens were used for fetching those messages", + description = "The token identifier(s) that tell you which auth-tokens were used for fetching those messages.", frozen = True, ) @@ -115,14 +115,14 @@ class SMSListRequestData(BaseModel): ) fromCount: int = Field( - description = "the no. of mails to skip before picking mails to list; useful for pagination", + description = "The no. of mails to skip before picking mails to list; useful for pagination.", ge = 0, default = 0, frozen = True ) tags: List[Any] | None = Field( - description = "any no. of tags that you want to filter by", + description = "Any no. of tags that you want to filter by.", default = None ) diff --git a/models/api/message/chat/send.py b/models/api/message/chat/send.py index c60b213..3e6a195 100644 --- a/models/api/message/chat/send.py +++ b/models/api/message/chat/send.py @@ -6,11 +6,11 @@ DATE: - Monday, 9th Dec., 2024. + Tuesday, 21st Jan., 2025. OBJECTIVE: - To provide a structure to receive API calls to send SMS messages from various third-party clients. + To provide a structure to receive API calls to send chat messages from various third-party clients. REFERENCES: @@ -46,11 +46,10 @@ from utils_v2.date_time import date_time # Models: from models.core.message import CoreMessageModel from utils_v2.sms.models.sms_message import SentSMSMessageModel -from models.message.sms.send import ( - NimbusSMSIndiaMessage, - SavvyBulkSMSKenyaMessage, - SMSSendOneResult, - SMSSendManyResults +from models.message.chat.send import ( + NimbusWhatsAppMessage, + ChatSendOneResult, + ChatSendManyResults ) # To work with date and time: @@ -88,7 +87,7 @@ REGEX_SESSION_TOKEN = r"^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9] # ***************************************************************************************************************** -class SMSSendRequestHeaders(BaseModel): +class ChatSendRequestHeaders(BaseModel): sessionToken: str | None = Field( description = "the session token of the user who is requesting the service", @@ -113,10 +112,10 @@ class SMSSendRequestHeaders(BaseModel): # --------------------------------------------------------------------------------------------------------------------- -class SMSSendRequestData(BaseModel): +class ChatSendRequestData(BaseModel): tokenKey: ObjectId - message: List[NimbusSMSIndiaMessage] | List[SavvyBulkSMSKenyaMessage] + message: List[NimbusWhatsAppMessage] tags: List[Any] | None = Field(default = None, validate_default = True) # ┏┓ ┏• diff --git a/models/api/message/chat/tags.py b/models/api/message/chat/tags.py index 655a276..7a7cee7 100644 --- a/models/api/message/chat/tags.py +++ b/models/api/message/chat/tags.py @@ -6,11 +6,11 @@ DATE: - Thursday, 19th Dec., 2024. + Tuesday, 21st Jan., 2025. OBJECTIVE: - To provide a structure to work with the tags on SMS messages. + To provide a structure to work with the tags on chat messages. REFERENCES: @@ -75,10 +75,10 @@ REGEX_SESSION_TOKEN = r"^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9] # ***************************************************************************************************************** -class SMSUpdateTagsRequestHeaders(BaseModel): +class ChatMessageUpdateTagsRequestHeaders(BaseModel): sessionToken: str = Field( - description = "the session token of the user who is requesting the service", + description = "The session token of the user who is requesting the service.", pattern = REGEX_SESSION_TOKEN, frozen = True, alias = "X-Session-Token" @@ -99,21 +99,21 @@ class SMSUpdateTagsRequestHeaders(BaseModel): # --------------------------------------------------------------------------------------------------------------------- -class SMSUpdateTagsRequestData(BaseModel): +class ChatMessageUpdateTagsRequestData(BaseModel): messageId: str = Field( - description = "the mail identifier (Mongo ObjectId) of the document that holds the mail", + description = "The message identifier (Mongo ObjectId) of the document that holds the chat message.", frozen = True ) unsetTags: List[Any] | None = Field( - description = "the list of tags to remove from the mail", + description = "The list of tags to remove from the message.", frozen = True, default = None ) setTags: List[Any] | None = Field( - description = "the list of tags to add to the mail", + description = "The list of tags to add to the message.", frozen = True, default = None ) diff --git a/models/message/chat/send.py b/models/message/chat/send.py index f002354..3c518be 100644 --- a/models/message/chat/send.py +++ b/models/message/chat/send.py @@ -6,11 +6,13 @@ DATE: - Monday, 9th Dec., 2024. + Tuesday, 21st Jan., 2025. OBJECTIVE: - To provide a structure to receive API calls to send SMS messages from various third-party clients. + To provide a structure to receive API calls to send chat messages from various third-party clients. At the time + of creating this file we are starting with Nimbus IT's unofficial WhatsApp services. We intend to add Telegram's + official APIs soon after. REFERENCES: @@ -36,7 +38,7 @@ sys.path.append(".") sys.path.append("..") # For making data behaviour_models: -from pydantic import BaseModel, Field, field_validator, PastDatetime +from pydantic import BaseModel, Field, field_validator, AwareDatetime, PastDatetime from typing import Optional, Literal, Union, List, Any # My utils: @@ -82,24 +84,42 @@ REGEX_SESSION_TOKEN = r"^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9] # ***************************************************************************************************************** -class NimbusSMSIndiaMessage(BaseModel): +class NimbusWhatsAppMessage(BaseModel): recipientNo: str = Field( - description = "the phone no. of the target recipient", - pattern = r"\+?\d{0,3}\s*\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}", + description = "The phone no. of the target recipient(s)", + # pattern = r"\+?\d{0,3}\s*\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}", frozen = True ) - text: str = Field( - description = "the actual text that you want to send", + message: str = Field( + description = "The actual text that you want to send.", min_length = 1, frozen = True ) - templateId: str = Field( - description = "the id of the template that you are trying to use to send the message", - min_length = 1, - frozen = True + pdfUrl: str | None = Field( + description = "A PDF file to send with your message.", + frozen = True, + default = None + ) + + image0Url: str | None = Field( + description = "An image file to send with your message.", + frozen = True, + default = None + ) + + image1Url: str | None = Field( + description = "An image file to send with your message.", + frozen = True, + default = None + ) + + scheduleTs: AwareDatetime | None = Field( + description = "The time (UTC) at which the message needs to be sent. Null for immediate delivery.", + frozen = True, + default = None ) # ┏┓ ┏• @@ -114,6 +134,25 @@ class NimbusSMSIndiaMessage(BaseModel): # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ + @field_validator("scheduleTs", mode = "before") + def to_datetime(cls, value): + if not isinstance(value, datetime.datetime): + value = date_time.parse_date_time( + input_value = value, + date_formats = [ + "%Y%m%d", + "%Y-%m-%d", + "%Y-%m-%d %H:%M:%S", + "%Y-%m-%d %H:%M:%S%z", + "%Y-%m-%dT%H:%M:%S", + "%Y-%m-%dT%H:%M:%S%z" + ] + ) + if value: + value = date_time.as_if_timezone(value, date_time.TIMEZONE_UTC) + value = date_time.to_timezone(value, date_time.TIMEZONE_IST) + return value + @field_validator("recipientNo", mode = "before") def validate_contact_nos(cls, value): value = regex.replace(text = str(value), pattern = r"[^\d]", substitute_text = "") @@ -125,46 +164,20 @@ class NimbusSMSIndiaMessage(BaseModel): # --------------------------------------------------------------------------------------------------------------------- -class SavvyBulkSMSKenyaMessage(BaseModel): - - recipientNo: str = Field( - description = "the phone no. of the target recipient", - min_length = 1, - frozen = True - ) - - text: str = Field( - description = "the actual text that you want to send", - min_length = 1, - frozen = True - ) - - # ┏┓ ┏• - # ┃ ┏┓┏┓╋┓┏┓ - # ┗┛┗┛┛┗┛┗┗┫ - # ┛ - - class Config: - extra = "forbid" - - -# --------------------------------------------------------------------------------------------------------------------- - - -class SMSSendOneResult(BaseModel): +class ChatSendOneResult(BaseModel): success: bool = Field( - description = "whether, or not, the sms was successfully sent", + description = "Whether, or not, the chat message was successfully sent.", default = False ) message: str | None = Field( - description = "a brief message to summarize the result of the process", + description = "A brief message to summarize the result of the process.", default = None ) - smsMessage: NimbusSMSIndiaMessage | SavvyBulkSMSKenyaMessage = Field( - description = "the actual data of the sms", + chatMessage: CoreMessageModel = Field( + description = "The actual data of the chat message.", default = None ) @@ -180,30 +193,30 @@ class SMSSendOneResult(BaseModel): # --------------------------------------------------------------------------------------------------------------------- -class SMSSendManyResults(BaseModel): +class ChatSendManyResults(BaseModel): totalCount: int = Field( - description = "the total no. of mails that were to be sync'd", + description = "The total no. of messages that were attempted.", default = 0 ) successCount: int = Field( - description = "the no. of mails that were successfully sync'd", + description = "The no. of chat messages that were successfully sent.", default = 0 ) failureCount: int = Field( - description = "the no. of mails that were successfully sync'd", + description = "The no. of chat messages that were NOT sent.", default = 0 ) message: str = Field( - description = "a brief message to summarize the results of the process", + description = "A brief message to summarize the results of the process.", default = None ) - smsMessages: List[CoreMessageModel] = Field( - description = "the actual data of the sms", + chatMessages: List[CoreMessageModel] = Field( + description = "The actual data of the chat messages.", default = [] )