diff --git a/api/blueprints/chat/auth.py b/api/blueprints/chat/auth.py index 6c7820d..30e5cab 100644 --- a/api/blueprints/chat/auth.py +++ b/api/blueprints/chat/auth.py @@ -60,7 +60,8 @@ from utils_v2.api.async_quart import ( ) # Data Models: -from models.data.api.chat.auth import ChatAuthRequestHeaders, ChatAuthRequestData +from models.api.chat.auth import ChatAuthRequestHeaders, ChatAuthRequestData +from models.core.auth_token import CoreAuthTokenModel # Common: from shared import constants @@ -198,10 +199,18 @@ async def callback_test( **kwargs ): - # ┏┓ - # ┃┃┏┓┏┓┏┓┏┓┏┓┏┏┓┏┏ - # ┣┛┛ ┗ ┣┛┛ ┗┛┗┗ ┛┛ - # ┛ + """ + Use this when a user wants to register a third-party chat client with your service. + :param inbound_headers: auto-extracted by the decorators. + :param inbound_data: auto-extracted by the decorators. + :param inbound_files: auto-extracted by the decorators. + :param kwargs: Any number of extra inputs supplied by the decorators. + :return: A standard response structure. + """ + + # ┏┓ ┓ ┏┓┓ ┓ + # ┣┫┓┏╋┣┓ ┃ ┣┓┏┓┏┃┏ + # ┛┗┗┻┗┛┗ ┗┛┛┗┗ ┗┛┗ # If the session token is invalid/expired: if kwargs.get("session_info") is None: @@ -210,22 +219,52 @@ async def callback_test( http_code = HttpCodes.UNAUTHORIZED ) - # ┏┓ ┏┳┓ ┓ - # ┣ ┏┓┏┓ ┃ ┏┓┃┏┓┏┓┏┓┏┓┏┳┓ - # ┻ ┗┛┛ ┻ ┗ ┗┗ ┗┫┛ ┗┻┛┗┗ - # ┛ + # Start by assuming failure: + success = False - if inbound_data.chatClient == "telegram": + # ┏┓ ┓ ┏┓ ┏┓ ┳┓• ┓ + # ┣ ┏┓┏┓ ┃┃┃┣┓┏┓╋┏┣┫┏┓┏┓━━┃┃┓┏┳┓┣┓┓┏┏ + # ┻ ┗┛┛ ┗┻┛┛┗┗┻┗┛┛┗┣┛┣┛ ┛┗┗┛┗┗┗┛┗┻┛ + # ┛ ┛ - # client_response = await get_telegram_bot_info(bot_token = inbound_data.auth.botToken) - # print("TG BOT:", json.to_string(client_response)) - client_response = await set_telegram_webhook( - bot_token = inbound_data.auth.botToken, - webhook_url = f"https://api.thecaoffice.com/converse/chat/webhook/{inbound_data.auth.botToken}" + if inbound_data.chatClient == "whatsappNimbus": + success = await current_app.whatsapp_nimbus_controller.set_token_direct( + sql_conn = current_app.sql_writer, + mongo_data_conn = current_app.data_mongo, + auth_token = CoreAuthTokenModel( + serviceType = "chat", + client = inbound_data.chatClient, + authType = "auth", + auth = inbound_data.auth.model_dump(), + user = kwargs.get("session_info"), + clientUserId = { + "senderId": inbound_data.auth.senderId + }, + status = "active", + syncFreq = 60 + ), + token_notes = { + "apiKey": inbound_data.auth.apiKey, + "senderId": inbound_data.auth.senderId + }, + display_name = inbound_data.auth.senderId, + session_token = inbound_headers["X-Session-Token"] ) - print("WEBHOOK JSON:", json.to_string(client_response)) - return "ok" + # ┳┓ + # ┣┫┏┓┏┏┓┏┓┏┓┏┏┓ + # ┛┗┗ ┛┣┛┗┛┛┗┛┗ + # ┛ + + # Done here: + return ResponseModel( + status_code = StatusCodes.OK if success else StatusCodes.FAILED, + http_code = HttpCodes.SUCCESS if success else HttpCodes.INTERNAL_SERVER_ERROR, + data = { + "client": inbound_data.chatClient, + "authorized": success + } + ) # ***************************************************************************************************************** diff --git a/api/main.py b/api/main.py index bea5092..6a19081 100644 --- a/api/main.py +++ b/api/main.py @@ -80,6 +80,9 @@ from controllers_v2.message.sms.all_sms import AllSMSController from controllers_v2.message.sms.nimbus_sms_india import NimbusSMSIndiaController from controllers_v2.message.sms.savvy_bulk_sms_kenya import SavvyBulkSMSKenyaController # --- +from controllers_v2.message.chat.all_chat import AllChatController +from controllers_v2.message.chat.whatsapp_nimbus import WhatsAppNimbusController +# --- from controllers_v2.finstitutions.trading.all_trading import AllTradingController from controllers_v2.finstitutions.trading.zerodha_kite import ZerodhaKiteTradingController from controllers_v2.finstitutions.trading.icici_breeze import ICICIBreezeTradingController @@ -110,7 +113,7 @@ from api.blueprints.sms.list import sms_list_bp from api.blueprints.sms.tags import sms_update_tags_bp # Chat Blueprints: -# from api.blueprints.chat.auth import chat_auth_bp +from api.blueprints.chat.auth import chat_auth_bp # from api.blueprints.chat.webhook import chat_webhook_bp # Software Blueprints: @@ -179,7 +182,7 @@ app.register_blueprint(sms_list_bp, url_prefix = f"/{MODULE_BASE}/sms") 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_auth_bp, url_prefix = f"/{MODULE_BASE}/chat") # app.register_blueprint(chat_webhook_bp, url_prefix = f"/{MODULE_BASE}/chat") # Software Blueprints: @@ -455,6 +458,20 @@ async def app_startup(**kwargs): debug = enable_debugging ) + # Messages / Chat Controllers: + current_app.chat_controller = AllChatController( + cache = current_app.module_cache, + http_client = current_app.http_client, + alert_url = current_app.script_data["alerts"]["url"], + debug = enable_debugging + ) + current_app.whatsapp_nimbus_controller = WhatsAppNimbusController( + cache = current_app.module_cache, + http_client = current_app.http_client, + alert_url = current_app.script_data["alerts"]["url"], + debug = enable_debugging + ) + # Finstitutions / Trading Controllers: current_app.trading_controller = AllTradingController( cache = current_app.module_cache, diff --git a/controllers_v2/message/chat/__init__.py b/controllers_v2/message/chat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/controllers_v2/message/chat/all_chat.py b/controllers_v2/message/chat/all_chat.py new file mode 100644 index 0000000..fb7d2a3 --- /dev/null +++ b/controllers_v2/message/chat/all_chat.py @@ -0,0 +1,147 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Wednesday, 15th Jan., 2025. + + OBJECTIVE: + + To handle all chat-related behaviour 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_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 + +# SMS Clients: +from utils_v2.sms.india.nimbus.controllers.async_nimbus import AsyncNimbusSMS + +# 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 AllChatController(ChatController): + + # ┏┓ + # ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓ + # ┗┛┗┛┛┗┛┗┛ ┗┻┗┗┗┛┛ + + def __init__( + self, + cache: AsyncRedisCache = None, + http_client: httpx.AsyncClient = None, + alert_url: str = None, + debug: bool = True, + debug_prefix: str = "All Chat (C) | ", + debug_only_errors: bool = True + ): + + """ + This is a common controller for chat-related activities when the third-party client is not known. + :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": "whatsappNimbus"}, + debug = debug, + debug_prefix = debug_prefix, + debug_only_errors = debug_only_errors + ) + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/controllers_v2/message/chat/base.py b/controllers_v2/message/chat/base.py new file mode 100644 index 0000000..f039690 --- /dev/null +++ b/controllers_v2/message/chat/base.py @@ -0,0 +1,162 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Wednesday, 15th Jan., 2025. + + OBJECTIVE: + + To handle all chat-related behaviour from one place. The initially known clients are WhatsApp and Telegram. + + 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.database.async_mongo_v2 import AsyncMongo +from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache + +# Controllers: +from controllers_v2.core.message import CoreMessageController + +# Models: +from models.core.auth_token import CoreAuthTokenModel +from models.api.sms.send import ( + NimbusSMSIndiaMessage, + SavvyBulkSMSKenyaMessage, + SMSSendOneResult, + SMSSendManyResults +) + +# 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 + +# To work with datatypes: +from typing import List, Any + +# To make HTTP requests: +import httpx + +# To make abstract classes: +from abc import ABC, abstractmethod + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** CLASSES *** +# ***** **** +# ***************************************************************************************************************** + + +class ChatController(CoreMessageController, ABC): + + # ┏┓ + # ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓ + # ┗┛┗┛┛┗┛┗┛ ┗┻┗┗┗┛┛ + + def __init__( + self, + cache: AsyncRedisCache = None, + http_client: httpx.AsyncClient = None, + alert_url: str = None, + base_filter: dict = None, + debug: bool = True, + debug_prefix: str = "Chat (C) | ", + debug_only_errors: bool = True + ): + + """ + This is the foundational controller for all chat services. This is built on top of the core message controller, + and, in turn, all individual chat client controllers must be built on top of this. + :param cache: The object to use for caching results from database calls. + :param http_client: The HTTP client + :param base_filter: The basic filter that will be applied to all fetching/updating queries. WARNING: THE BASE + FILTER WILL ALWAYS BE APPLIED AUTOMATICALLY. SET THIS UP WISELY. + :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. + """ + + # Prepare the combined base filter: + sms_filter = {} + for k, v in (base_filter or {}).items(): sms_filter[k] = v + sms_filter["serviceType"] = "sms" + + # Invoke the parent's constructor: + CoreMessageController.__init__( + self, + cache = cache, + alert_url = alert_url, + http_client = http_client, + base_filter = sms_filter, + debug = debug, + debug_prefix = debug_prefix, + debug_only_errors = debug_only_errors + ) + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/controllers_v2/message/chat/whatsapp_nimbus.py b/controllers_v2/message/chat/whatsapp_nimbus.py new file mode 100644 index 0000000..a87641a --- /dev/null +++ b/controllers_v2/message/chat/whatsapp_nimbus.py @@ -0,0 +1,147 @@ +""" + + 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_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 + +# SMS Clients: +from utils_v2.sms.india.nimbus.controllers.async_nimbus import AsyncNimbusSMS + +# 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): + + # ┏┓ + # ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓ + # ┗┛┗┛┛┗┛┗┛ ┗┻┗┗┗┛┛ + + 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": "whatsappNimbus"}, + debug = debug, + debug_prefix = debug_prefix, + debug_only_errors = debug_only_errors + ) + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/models/api/chat/auth.py b/models/api/chat/auth.py index 3dd6a26..937fdaa 100644 --- a/models/api/chat/auth.py +++ b/models/api/chat/auth.py @@ -95,6 +95,32 @@ class TelegramAuth(BaseModel): # --------------------------------------------------------------------------------------------------------------------- +class WhatsAppNimbusAuth(BaseModel): + + apiKey: str = Field( + description = "???", + min_length = 1, + frozen = True + ) + + senderId: str = Field( + description = "???", + min_length = 1, + frozen = True + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + +# --------------------------------------------------------------------------------------------------------------------- + + class ChatAuthRequestHeaders(BaseModel): sessionToken: str = Field( @@ -121,8 +147,12 @@ class ChatAuthRequestHeaders(BaseModel): class ChatAuthRequestData(BaseModel): - chatClient: Literal["telegram", "whatsapp"] = Field(alias = "client") - auth: Union[TelegramAuth] + chatClient: Literal[ + "telegram", # ........ Official Telegram API. + "whatsapp", # ........ Official WhatsApp API. + "whatsappNimbus" # ... Nimbus IT's unofficial WhatsApp API. + ] = Field(alias = "client") + auth: Union[TelegramAuth, WhatsAppNimbusAuth] # ┏┓ ┏• # ┃ ┏┓┏┓╋┓┏┓ @@ -138,10 +168,12 @@ class ChatAuthRequestData(BaseModel): @model_validator(mode = "after") def ensure_harmony(cls, values): - client = values.client + client = values.chatClient auth = values.auth harmony_map = { - "telegram": TelegramAuth + "telegram": TelegramAuth, + # "whatsapp": None, + "whatsappNimbus": WhatsAppNimbusAuth } if not isinstance(auth, harmony_map[client]): raise ValueError(f"incorrect 'auth' for selected client '{client}'") diff --git a/models/core/auth_token.py b/models/core/auth_token.py index a61ae3d..164cade 100644 --- a/models/core/auth_token.py +++ b/models/core/auth_token.py @@ -106,7 +106,7 @@ class CoreAuthTokenModel(BaseModel): client: Literal[ "gmail", "outlook", # ............................. Mail Clients - "telegram", "whatsapp", # ......................... Chat Clients + "telegram", "whatsapp", "whatsappNimbus", # ....... Chat Clients "nimbusSmsIndia", "savvyBulkSmsKenya", # .......... SMS Clients "razorpay", "safaricomMPesaExpress", # ............ Payment Gateways "zerodhaKite", "iciciBreeze", "paperTrading", # ... Stock Brokers diff --git a/models/core/message.py b/models/core/message.py index 2a01c62..6d1d3be 100644 --- a/models/core/message.py +++ b/models/core/message.py @@ -112,9 +112,9 @@ class CoreMessageModel(BaseModel): ) client: Literal[ - "gmail", "outlook", # ...................... Mail Clients - "telegram", "whatsapp", # .................. Chat Clients - "nimbusSmsIndia", "savvyBulkSmsKenya", # ... SMS Clients + "gmail", "outlook", # ........................................ Mail Clients + "telegram", "whatsapp", "whatsappNimbus", # .................. Chat Clients + "nimbusSmsIndia", "savvyBulkSmsKenya", # ..................... SMS Clients ] = Field( description = "the third-part client that was used", frozen = True