From 4f5668c0367e0436bc263e76ba85472cab76bb53 Mon Sep 17 00:00:00 2001 From: khushal Date: Fri, 13 Dec 2024 18:28:45 +0530 Subject: [PATCH] (20241213) SMS Auth reworked. --- api/blueprints/sms/auth.py | 39 +- api/blueprints/sms/send.py | 18 +- api/blueprints/software/auth.py | 2 +- api/main.py | 6 +- controllers/api/sms.py | 371 ++++++++++++++++++ models/api/sms/send.py | 81 +++- .../models/behaviour/nimbus/async_nimbus.py | 2 +- .../savvy_bulk_sms/async_savvy_bulk_sms.py | 2 +- utils_v2/sms/models/data/sms_message.py | 2 +- 9 files changed, 476 insertions(+), 47 deletions(-) create mode 100644 controllers/api/sms.py diff --git a/api/blueprints/sms/auth.py b/api/blueprints/sms/auth.py index 6508f25..3fd14b0 100644 --- a/api/blueprints/sms/auth.py +++ b/api/blueprints/sms/auth.py @@ -63,8 +63,8 @@ from utils_v2.api.async_quart import ( from shared import constants # Data Models: -from models.data.api.sms.auth import SMSAuthRequestHeaders, SMSAuthRequestData -from models.data.core.auth_token import CoreAuthTokenModel +from models.api.sms.auth import SMSAuthRequestHeaders, SMSAuthRequestData +from models.core.auth_token import CoreAuthTokenModel # For asynchronous activities: import asyncio @@ -158,7 +158,7 @@ async def authorize_sms_client( ) # Start by assuming failure: - token_id = None + success = False # ┏┓ ┳┓• ┓ ┏┓┳┳┓┏┓ ┳ ┓• # ┣ ┏┓┏┓ ┃┃┓┏┳┓┣┓┓┏┏ ┗┓┃┃┃┗┓ ┃┏┓┏┫┓┏┓ @@ -166,7 +166,7 @@ async def authorize_sms_client( if inbound_data.smsClient == "nimbusSmsIndia": - token_id = await current_app.sms_auth_model.set( + success = await current_app.sms_controller.set_token( db_conn = current_app.sql_writer, mongo_conn = current_app.data_mongo, auth_token = CoreAuthTokenModel( @@ -183,17 +183,6 @@ async def authorize_sms_client( status = "active", syncFreq = 60 ), - # user_info = kwargs["session_info"], - # client_user_id = { - # "userId": inbound_data.auth.userId, - # "senderId": inbound_data.auth.senderId, - # "entityId": inbound_data.auth.entityId - # }, - # auth = inbound_data.auth.model_dump(), - # token = None, - # service_client = inbound_data.smsClient, - # auth_type = "auth", - # sync_freq = 300, session_token = inbound_headers["X-Session-Token"] ) @@ -204,10 +193,10 @@ async def authorize_sms_client( elif inbound_data.smsClient == "savvyBulkSmsKenya": - token_id = await current_app.sms_auth_model.set( + success = await current_app.sms_controller.set_token( db_conn = current_app.sql_writer, mongo_conn = current_app.data_mongo, - auth_token=CoreAuthTokenModel( + auth_token = CoreAuthTokenModel( serviceType = "sms", client = inbound_data.smsClient, authType = "auth", @@ -220,16 +209,6 @@ async def authorize_sms_client( status = "active", syncFreq = 60 ), - # user_info = kwargs["session_info"], - # client_user_id = { - # "partnerId": inbound_data.auth.partnerId, - # "shortCode": inbound_data.auth.shortCode - # }, - # auth = inbound_data.auth.model_dump(), - # token = None, - # service_client = inbound_data.smsClient, - # auth_type = "auth", - # sync_freq = 300, session_token = inbound_headers["X-Session-Token"] ) @@ -240,11 +219,11 @@ async def authorize_sms_client( # Done here: return ResponseModel( - status_code = StatusCodes.OK if token_id else StatusCodes.FAILED, - http_code = HttpCodes.SUCCESS if token_id else HttpCodes.INTERNAL_SERVER_ERROR, + status_code = StatusCodes.OK if success else StatusCodes.FAILED, + http_code = HttpCodes.SUCCESS if success else HttpCodes.INTERNAL_SERVER_ERROR, data = { "client": inbound_data.smsClient, - "authorized": True + "authorized": success } ) diff --git a/api/blueprints/sms/send.py b/api/blueprints/sms/send.py index 5436f1f..dbf1472 100644 --- a/api/blueprints/sms/send.py +++ b/api/blueprints/sms/send.py @@ -60,8 +60,8 @@ from utils_v2.api.async_quart import ( ) # Data Models: -from models.data.api.sms.send import SMSSendRequestHeaders, SMSSendRequestData -from models.data.core.auth_token import CoreAuthTokenModel +from models.core.auth_token import CoreAuthTokenModel +from models.api.sms.send import SMSSendRequestHeaders, SMSSendRequestData # Common: from shared import constants @@ -174,13 +174,13 @@ async def send_sms( # ┗┓┏┓┏┓┏┫ ┃ ┣┓┏┓ ┗┓┃┃┃┗┓ # ┗┛┗ ┛┗┗┻ ┻ ┛┗┗ ┗┛┛ ┗┗┛ - client_response = await current_app.sms_send_model.send_sms( - mongo_conn = current_app.data_mongo, - token_id = inbound_data.tokenId, - auth_token = auth_token, - inbound_data = inbound_data, - session_token = inbound_headers["X-Session-Token"] - ) + # client_response = await current_app.sms_send_model.send_sms( + # mongo_conn = current_app.data_mongo, + # token_id = inbound_data.tokenId, + # auth_token = auth_token, + # inbound_data = inbound_data, + # session_token = inbound_headers["X-Session-Token"] + # ) # ┳┓ # ┣┫┏┓┏┏┓┏┓┏┓┏┏┓ diff --git a/api/blueprints/software/auth.py b/api/blueprints/software/auth.py index 8df6e13..1517d20 100644 --- a/api/blueprints/software/auth.py +++ b/api/blueprints/software/auth.py @@ -208,7 +208,7 @@ async def authorize_software_client( http_code = HttpCodes.SUCCESS if success else HttpCodes.INTERNAL_SERVER_ERROR, data = { "client": inbound_data.softwareClient, - "authorized": True if success else False + "authorized": success } ) diff --git a/api/main.py b/api/main.py index 915b42e..bdcc3be 100644 --- a/api/main.py +++ b/api/main.py @@ -69,6 +69,7 @@ from controllers.core.ai.llm import LLMController # API Controller Models: from controllers.api.mail import MailController +from controllers.api.sms import SMSController # # Old Behaviour Models: # from controllers.mail.oauth_v3 import MailOAuthModel @@ -90,7 +91,7 @@ from api.blueprints.mail.sync.sync_v2 import mail_sync_bp from api.blueprints.mail.retrieve.list import mail_list_bp from api.blueprints.mail.retrieve.get import mail_get_bp from api.blueprints.mail.tags.update import mail_tags_update_bp -# from api.blueprints.sms.auth import sms_auth_bp +from api.blueprints.sms.auth import sms_auth_bp # from api.blueprints.sms.send import sms_send_bp # from api.blueprints.chat.auth import chat_auth_bp # from api.blueprints.chat.webhook import chat_webhook_bp @@ -131,7 +132,7 @@ app.register_blueprint(mail_sync_bp, url_prefix = f"/{MODULE_BASE}/mail") app.register_blueprint(mail_list_bp, url_prefix = f"/{MODULE_BASE}/mail") app.register_blueprint(mail_get_bp, url_prefix = f"/{MODULE_BASE}/mail") app.register_blueprint(mail_tags_update_bp, url_prefix = f"/{MODULE_BASE}/mail") -# app.register_blueprint(sms_auth_bp, url_prefix = f"/{MODULE_BASE}/sms") +app.register_blueprint(sms_auth_bp, url_prefix = f"/{MODULE_BASE}/sms") # app.register_blueprint(sms_send_bp, url_prefix = f"/{MODULE_BASE}/sms") # app.register_blueprint(chat_auth_bp, url_prefix = f"/{MODULE_BASE}/chat") # app.register_blueprint(chat_webhook_bp, url_prefix = f"/{MODULE_BASE}/chat") @@ -347,6 +348,7 @@ async def app_startup(**kwargs): # ┛┗┣┛┻ ┗┛┗┛┛┗┗┛ ┗┛┗┗┗ ┛ ┛ current_app.mail_controller = MailController() + current_app.sms_controller = SMSController() # ┏┓ ┓ ┏┓┓• # ┃ ┏┓┏┓┏┓┏┓┏╋┏┓┏┓┏ ┏┓┏┓┏┫ ┃ ┃┓┏┓┏┓╋┏ diff --git a/controllers/api/sms.py b/controllers/api/sms.py new file mode 100644 index 0000000..758e594 --- /dev/null +++ b/controllers/api/sms.py @@ -0,0 +1,371 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Friday, 13th Dec., 2024 + + OBJECTIVE: + + To handle all SMS related behaviour from one place. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + +""" + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys + +import httpx +from google.protobuf.duration import from_microseconds + +sys.path.append(".") +sys.path.append("..") + +# For Quart: +from quart import current_app + +# My async utils: +from utils_v2.string import json +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, AsyncMongoStorage + +# Base model: +from controllers.base import BaseModel + +# Data models: +from models.core.user import CoreUserInfoModel +from models.core.auth_token import CoreAuthTokenModel +from models.core.message import CoreMessageModel +from models.api.sms.send import ( + SMSSendRequestData, + NimbusSMSIndiaMessage, + SavvyBulkSMSKenyaMessage, + SMSSendManyResults +) + +# SMS Clients: +from utils_v2.sms.models.behaviour.nimbus.async_nimbus import AsyncNimbusSMS +from utils_v2.sms.models.behaviour.savvy_bulk_sms.async_savvy_bulk_sms import AsyncSavvyBulkSMS +from utils_v2.sms.models.data.sms_message import SentSMSMessageModel + +# To work with MongoDB: +from bson import ObjectId + +# To work with datatypes: +from typing import Literal, List, Dict, Any + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** CLASSES *** +# ***** **** +# ***************************************************************************************************************** + + +class SMSController: + + # ┏┓┓ ┓┏ + # ┃ ┃┏┓┏┏ ┃┃┏┓┏┓┏ + # ┗┛┗┗┻┛┛ ┗┛┗┻┛ ┛ + + pass + + # ┓┏ ┓ + # ┣┫┏┓┃┏┓┏┓┏┓┏ + # ┛┗┗ ┗┣┛┗ ┛ ┛ + # ┛ + + pass + + # ┏┓ ┓ + # ┣┫┓┏╋┣┓ + # ┛┗┗┻┗┛┗ + + @staticmethod + async def set_token( + db_conn: AsyncMySQL, + mongo_conn: AsyncMongo, + auth_token: CoreAuthTokenModel, + session_token: str = None + ) -> bool: + + # Start by assuming failure: + success = False + + # Get a token id: + token_id = await current_app.core_auth_token_controller.get_token_id( + db_conn = db_conn, + mongo_conn = mongo_conn, + auth_token = auth_token, + token_notes = {}, + session_token = session_token + ) + + # Immediately save the details against that token id: + success = await current_app.core_auth_token_controller.set_token( + db_conn = db_conn, + mongo_conn = mongo_conn, + token_id = token_id, + auth_token = auth_token, + token_notes = {}, + session_token = session_token + ) + + # Done here: + return success + + @staticmethod + async def get_token( + mongo_conn: AsyncMongo, + token_id: ObjectId | str = None, + ) -> CoreAuthTokenModel | None: + + # Simply call the core model: + return await current_app.core_auth_token_controller.get_token( + mongo_conn = mongo_conn, + token_id = token_id + ) + + # ┏┓ ┓ + # ┗┓┏┓┏┓┏┫ + # ┗┛┗ ┛┗┗┻ + + @staticmethod + async def __send_from_nimbus_sms_india( + http_client: httpx.AsyncClient, + token_id: ObjectId | str, + auth_token: CoreAuthTokenModel, + messages: List[NimbusSMSIndiaMessage], + ) -> SMSSendManyResults: + + # Start with a blank variable: + send_results = SMSSendManyResults() + + # Initialize the third-party client: + client = AsyncNimbusSMS( + entity_id = auth_token.auth["entityId"], + sender_id = auth_token.auth["senderId"], + user_id = auth_token.auth["userId"], + api_key = auth_token.auth["apiKey"], + http_client = http_client + ) + + # Iterate over all the messages you need to send: + for message in messages: + + # Send the SMS and return the response: + client_response = await client.send_sms( + recipient_number = message.recipientNo, + message = message.text, + template_id = message.templateId + ) + + # Note down the results: + send_results.totalCount += 1 + if client_response.success: send_results.successCount += 1 + else: send_results.failureCount += 1 + send_results.smsMessages.append(CoreMessageModel( + ts = client_response.ts, + syncTs = date_time.get_current_utc_date_time(as_string = False), + tokenId = ObjectId(token_id), + serviceType = auth_token.serviceType, + client = auth_token.client, + clientMessageId = client_response.messageId, + clientThreadId = message.recipientNo, + isSent = True, + isBroadcast = False, + sentSuccessfully = client_response.success, + sender = None, + chat = None, + message = client_response.model_dump(), + snippet = message.text, + aiSnippet = None, + tags = ["sms", "nimbusSmsIndia"] + )) + + # Done here: + return send_results + + async def send( + self, + mongo_conn: AsyncMongo, + http_client: httpx.AsyncClient, + token_id: ObjectId | str, + messages: List[NimbusSMSIndiaMessage | SavvyBulkSMSKenyaMessage], + session_token: str + ) -> SMSSendManyResults: + + # Start by assuming failure: + send_results = SMSSendManyResults() + + # We first load the authorization tokens: + auth_token = await self.get_token( + mongo_conn = mongo_conn, + token_id = token_id, + ) + + # If we failed to load the authorization tokens: + if not auth_token: + send_results.message = f"no such token id '{token_id}'" + return send_results + + # Now we route the message to the appropriate client: + match auth_token.client: + case "nimbusSmsIndia": + send_results = await self.__send_from_nimbus_sms_india( + http_client = http_client, + token_id = token_id, + auth_token = auth_token, + messages = messages + ) + case "savvyBulkSmsKenya": + pass + case _: + send_results.message = f"invalid client {auth_token.client}" + + # Save the results to MongoDB: + + + # Done here: + return send_results + + # ┓ • ┏┓ ┏┓ ┳┳┓ + # ┃ ┓┏╋ ┣╋ ┃┓┏┓╋ ┃┃┃┏┓┏┏┏┓┏┓┏┓┏ + # ┗┛┗┛┗ ┗┻ ┗┛┗ ┗ ┛ ┗┗ ┛┛┗┻┗┫┗ ┛ + # ┛ + + # These are simply for retrieving sms messages. + # You need to already have them saved to the database. + + # @staticmethod + # async def list_messages( + # mongo_conn: AsyncMongo, + # token_ids: List[ObjectId | str], + # limit: int = 100, + # skip: int = 0, + # additional_filter: dict = None + # ) -> List[CoreMessageModel] | None: + # + # # regardless of what additional filter is provided from outside, + # # we add a mail-selecting filter here: + # if additional_filter is None: additional_filter = {} + # additional_filter["serviceType"] = "sms" + # + # # Simply call the core model: + # return await current_app.core_message_controller.get_message( + # mongo_conn = mongo_conn, + # token_ids = token_ids, + # limit = limit, + # skip = skip, + # additional_filter = additional_filter + # ) + # + # @staticmethod + # async def get_one_mail( + # mongo_conn: AsyncMongo, + # token_id: ObjectId | str, + # message_id: ObjectId | str + # ) -> CoreMessageModel | None: + # + # # Simply call the core model: + # return await current_app.core_message_controller.get_message( + # mongo_conn = mongo_conn, + # token_id = token_id, + # message_id = message_id + # ) + + # ┳┳ ┓ + # ┃┃┏┓┏┫┏┓╋┏┓ + # ┗┛┣┛┗┻┗┻┗┗ + # ┛ + + @staticmethod + async def update_tags( + mongo_conn: AsyncMongo, + token_id: ObjectId | str, + message_id: ObjectId | str, + unset_tags: List[str] = None, + set_tags: List[str] = None + ) -> bool: + + # Simply call the core model: + return await current_app.core_message_controller.update_tags( + mongo_conn = mongo_conn, + token_id = token_id, + message_id = message_id, + unset_tags = unset_tags, + set_tags = set_tags + ) + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass + + # from utils_v2.string import json + # + # file_options = [ + # r"/home/developer/Downloads/recursive parts parse - 20241210.json", + # r"/home/developer/Downloads/recursive parts parse (no attachment) - 20241210.json", + # ] + # + # raw_mail_json = json.from_file(file_options[1]) + # print("FROM FILE:", json.to_string(raw_mail_json["payload"])) + # print("\n\n---------\n\n") + # mail_controller = MailController() + # print(json.to_string(mail_controller.drop_attachments(raw_mail_json["payload"]))) diff --git a/models/api/sms/send.py b/models/api/sms/send.py index f149c7b..cc21138 100644 --- a/models/api/sms/send.py +++ b/models/api/sms/send.py @@ -37,12 +37,16 @@ sys.path.append("..") # For making data behaviour_models: from pydantic import BaseModel, Field, field_validator, PastDatetime -from typing import Optional, Literal, Union +from typing import Optional, Literal, Union, List # My utils: from utils_v2.string import regex from utils_v2.date_time import date_time +# Data models: +from models.core.message import CoreMessageModel +from utils_v2.sms.models.data.sms_message import SentSMSMessageModel + # To work with date and time: import datetime @@ -174,7 +178,7 @@ class SMSSendRequestHeaders(BaseModel): class SMSSendRequestData(BaseModel): tokenId: ObjectId = Field(description = "the auth token to use to send this message") - message: Union[NimbusSMSIndiaMessage, SavvyBulkSMSKenyaMessage] + message: Union[List[NimbusSMSIndiaMessage], List[SavvyBulkSMSKenyaMessage]] # ┏┓ ┏• # ┃ ┏┓┏┓╋┓┏┓ @@ -195,6 +199,79 @@ class SMSSendRequestData(BaseModel): except: pass return value + @field_validator("message", mode = "before") + def ensure_list(cls, value): + if not isinstance(value, list): value = [value] + return value + + +# --------------------------------------------------------------------------------------------------------------------- + + +class SMSSendOneResult(BaseModel): + + success: bool = Field( + description = "whether, or not, the sms was successfully sent", + default = False + ) + + message: str | None = Field( + description = "a brief message to summarize the result of the process", + default = None + ) + + smsMessage: Union[NimbusSMSIndiaMessage, SavvyBulkSMSKenyaMessage] = Field( + description = "the actual data of the sms", + default = None + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + +# --------------------------------------------------------------------------------------------------------------------- + + +class SMSSendManyResults(BaseModel): + + totalCount: int = Field( + description = "the total no. of mails that were to be sync'd", + default = 0 + ) + + successCount: int = Field( + description = "the no. of mails that were successfully sync'd", + default = 0 + ) + + failureCount: int = Field( + description = "the no. of mails that were successfully sync'd", + default = 0 + ) + + message: str = Field( + description = "a brief message to summarize the results of the process", + default = None + ) + + smsMessages: List[CoreMessageModel] = Field( + description = "the actual data of the sms", + default = [] + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + # ***************************************************************************************************************** # ***** **** diff --git a/utils_v2/sms/models/behaviour/nimbus/async_nimbus.py b/utils_v2/sms/models/behaviour/nimbus/async_nimbus.py index d36791b..5999dd0 100644 --- a/utils_v2/sms/models/behaviour/nimbus/async_nimbus.py +++ b/utils_v2/sms/models/behaviour/nimbus/async_nimbus.py @@ -234,7 +234,7 @@ class AsyncNimbusSMS: summary.rawResponse = response_json summary.success = True if response_json.get("STATUS", "ERROR").lower() in ["ok"] else False if summary.success: summary.messageId = response_json.get("RESPONSE", {}).get("UID") - else: summary.brief = response_json.get("RESPONSE", {}).get("INFO") + else: summary.message = response_json.get("RESPONSE", {}).get("INFO") # For any other code that indicates some form of failure: else: summary.rawResponse = response.content.decode() diff --git a/utils_v2/sms/models/behaviour/savvy_bulk_sms/async_savvy_bulk_sms.py b/utils_v2/sms/models/behaviour/savvy_bulk_sms/async_savvy_bulk_sms.py index d362632..ef23a93 100644 --- a/utils_v2/sms/models/behaviour/savvy_bulk_sms/async_savvy_bulk_sms.py +++ b/utils_v2/sms/models/behaviour/savvy_bulk_sms/async_savvy_bulk_sms.py @@ -188,7 +188,7 @@ class AsyncSavvyBulkSMS: first_desc = first_response.get("response-description", "N/A").lower().strip() summary.success = True if first_desc == "success" else False if summary.success: summary.messageId = first_response.get("messageid") - else: summary.brief = first_response.get("response-description") + else: summary.message = first_response.get("response-description") # For any other code that indicates some form of failure: else: summary.rawResponse = api_response.content.decode() diff --git a/utils_v2/sms/models/data/sms_message.py b/utils_v2/sms/models/data/sms_message.py index 0d37beb..44c5a18 100644 --- a/utils_v2/sms/models/data/sms_message.py +++ b/utils_v2/sms/models/data/sms_message.py @@ -115,7 +115,7 @@ class SentSMSMessageModel(BaseModel): default = False ) - brief: str | None = Field( + message: str | None = Field( description = "a brief message about what happened; useful when something goes wrong", default = None )