""" AUTHOR: Khushal P Soonderji DATE: Wednesday, 15th Jan., 2025. OBJECTIVE: To handle all WhatsApp-related behaviour for Nimbus IT's service from one place. REFERENCES: N/A DOWNLOADS: N/A """ # ***************************************************************************************************************** # ***** **** # *** IMPORT *** # ***** **** # ***************************************************************************************************************** # To make sibling directories accessible for imports: import sys 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.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 ) # Chat clients: from utils_v2.whatsapp.nimbus.controllers.async_nimbus_whatsapp import AsyncNimbusWhatsapp # To work with datatypes: from typing import List, Any # To make HTTP requests: import httpx # For asynchronous activities: import asyncio # ***************************************************************************************************************** # ***** **** # *** MACROS / ONE-TIME INIT *** # ***** **** # ***************************************************************************************************************** # --- Nothing Yet # ***************************************************************************************************************** # ***** **** # *** VARIABLES *** # ***** **** # ***************************************************************************************************************** # --- Nothing Yet # ***************************************************************************************************************** # ***** **** # *** FUNCTIONS *** # ***** **** # ***************************************************************************************************************** # --- Nothing Yet # ***************************************************************************************************************** # ***** **** # *** CLASSES *** # ***** **** # ***************************************************************************************************************** class WhatsAppNimbusController(ChatController): # ┏┓┓ ┓┏ # ┃ ┃┏┓┏┏ ┃┃┏┓┏┓┏ # ┗┛┗┗┻┛┛ ┗┛┗┻┛ ┛ CLIENT_NAME = "whatsappNimbus" # ┏┓ # ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓ # ┗┛┗┛┛┗┛┗┛ ┗┻┗┗┗┛┛ def __init__( self, cache: AsyncRedisCache = None, http_client: httpx.AsyncClient = None, alert_url: str = None, debug: bool = True, debug_prefix: str = "WhatsApp Nimbus (C) | ", debug_only_errors: bool = True ): """ This is the controller for Nimbus IT's WhatsApp service. :param cache: The object to use for caching results from database calls. :param http_client: The HTTP client :param debug: Whether, or not, you would like to print debugging messages: :param debug_prefix: The prefix to print with the debugging messages. :param debug_only_errors: Whether you would like to print only error messages or all messages. :return: None. """ # Invoke the parent's constructor: super().__init__( cache = cache, alert_url = alert_url, http_client = http_client, base_filter = {"client": self.CLIENT_NAME}, debug = debug, debug_prefix = debug_prefix, debug_only_errors = debug_only_errors ) # Init a variable in a parent: self._client = self.CLIENT_NAME # ┏┓ ┓ ┳┳┓ # ┗┓┏┓┏┓┏┫ ┃┃┃┏┓┏┏┏┓┏┓┏┓┏ # ┗┛┗ ┛┗┗┻ ┛ ┗┗ ┛┛┗┻┗┫┗ ┛ # ┛ 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 # ***************************************************************************************************************** # ***** **** # *** MAIN PROGRAM *** # ***** **** # ***************************************************************************************************************** if __name__ == "__main__": pass