diff --git a/api/blueprints/message/chat/list.py b/api/blueprints/message/chat/list.py new file mode 100644 index 0000000..dab5c38 --- /dev/null +++ b/api/blueprints/message/chat/list.py @@ -0,0 +1,230 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Thursday, 19th Dec., 2024 + + OBJECTIVE: + + To list SMS messages associated with incoming identifiers. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + + NOTES: + + N/A + +""" + + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# For using Quart: +from quart import Blueprint, current_app, request + +# My utils: +from utils_v2.string import json +from utils_v2.api.codes import StatusCodes, HttpCodes +from utils_v2.api.response import ResponseModel +from utils_v2.api.async_quart import ( + set_api_version, + read_input, + get_session_info, + log_request_to_mongo, + log_chain_to_mongo, + should_not_be_under_maintenance, + only_whitelisted_ips, + limit_rate, + validate_input, + handle_cancelled_request +) + +# Common: +from shared import constants + +# Data Models: +from models.core.user import CoreUserInfoModel +from models.api.message.sms.list import SMSListRequestHeaders, SMSListRequestData + +# Helpers: +from api.helpers.user import token_check + +# To work with MongoDB: +from bson import ObjectId + +# For asynchronous activities: +import asyncio + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# Related to Quart: +sms_list_bp = Blueprint("sms_list", __name__) + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +@sms_list_bp.record_once +def init(blueprint_setup_state): + + # This gets called when the blueprint is registered. + # Consider this to be a one-time setup for the whole blueprint: + pass + + +# --------------------------------------------------------------------------------------------------------------------- + + +@sms_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") +@log_request_to_mongo( + attr_name = "logs_mongo", + project = constants.PROJECT_NAME, + log_type = constants.MODULE_NAME, + operation = "smsListApi", + log_input = True, + log_output = True, + sensitive_keys = ["sessionToken", "X-Session-Token", "tokenKeys"] +) +@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) +) +@handle_cancelled_request() +async def list_sms_messages( + inbound_headers: dict | SMSListRequestHeaders = None, + inbound_data: dict | SMSListRequestData = None, + inbound_files: dict = None, + **kwargs +): + + """ + Use this API when a user wants his SMS messages listed on the screen. + :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: + return ResponseModel( + status_code = StatusCodes.FAILED, + http_code = HttpCodes.UNAUTHORIZED + ) + + # ┏┓ ┓ • ┏┓┓ ┓ + # ┃┃┓┏┏┏┓┏┓┏┓┏┣┓┓┏┓ ┃ ┣┓┏┓┏┃┏ + # ┗┛┗┻┛┛┗┗ ┛ ┛┛┗┗┣┛ ┗┛┛┗┗ ┗┛┗ + # ┛ + + # Get the tokens from the database: + auth_tokens = await current_app.sms_controller.get_tokens_from_keys( + mongo_data_conn = current_app.data_mongo, + token_keys = inbound_data.tokenKeys, + limit = len(inbound_data.tokenKeys) + ) + token_ids = [ObjectId(t.authTokenId) for t in auth_tokens] + + # Check if these tokens belong to the user claiming ownership: + if not await token_check.is_authorized( + mongo_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." + ) + + # ┳┓ ┓ • • + # ┃┃┏┓╋┏┓ ┃ ┓┏╋┓┏┓┏┓ + # ┻┛┗┻┗┗┻ ┗┛┗┛┗┗┛┗┗┫ + # ┛ + + messages = await current_app.sms_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 + } + ) + + # ┳┓ + # ┣┫┏┓┏┏┓┏┓┏┓┏┏┓ + # ┛┗┗ ┛┣┛┗┛┛┗┛┗ + # ┛ + + # Done here: + message_count = len(messages) + success = True if messages is not None and message_count > 0 else False + return ResponseModel( + 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." + ) + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/api/blueprints/message/chat/send.py b/api/blueprints/message/chat/send.py new file mode 100644 index 0000000..12e550c --- /dev/null +++ b/api/blueprints/message/chat/send.py @@ -0,0 +1,272 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Thursday, 19th Dec., 2024 + + OBJECTIVE: + + To send SMS messages through various third-party clients. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + + NOTES: + + N/A + +""" + + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# For using Quart: +from quart import Blueprint, current_app, request + +# My utils: +from utils_v2.string import json +from utils_v2.api.codes import StatusCodes, HttpCodes +from utils_v2.database.async_mongo_v2 import AsyncMongo +from utils_v2.api.response import ResponseModel +from utils_v2.api.async_quart import ( + set_api_version, + read_input, + get_session_info, + log_request_to_mongo, + log_chain_to_mongo, + should_not_be_under_maintenance, + only_whitelisted_ips, + limit_rate, + validate_input, + handle_cancelled_request +) + +# 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 +) + +# Common: +from shared import constants + +# To work with datatypes: +from typing import List, Any + +# To make HTTP requests: +import httpx + +import asyncio + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# Related to Quart: +sms_send_bp = Blueprint("sms_send", __name__) + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +@sms_send_bp.record_once +def init(blueprint_setup_state): + + # This gets called when the blueprint is registered. + # Consider this to be a one-time setup for the whole blueprint: + pass + + +# --------------------------------------------------------------------------------------------------------------------- + + +async def send_sms_messages( + mongo_data_conn: AsyncMongo, + auth_token: CoreAuthTokenModel, + messages: List[NimbusSMSIndiaMessage | SavvyBulkSMSKenyaMessage], + tags: List[Any] +) -> SMSSendManyResults: + + """ + 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. + """ + + # Start by assuming failure: + results = SMSSendManyResults() + + # 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 _: + results.message = "Invalid/unimplemented SMS client." + + # Done here: + return results + + +# --------------------------------------------------------------------------------------------------------------------- + + +@sms_send_bp.route("", methods = ["POST"]) +@sms_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") +@log_request_to_mongo( + attr_name = "logs_mongo", + project = constants.PROJECT_NAME, + log_type = constants.MODULE_NAME, + operation = "smsSendApi", + log_input = True, + 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) +) +@handle_cancelled_request() +async def send_sms_messages_api( + inbound_headers: dict | SMSSendRequestHeaders = None, + inbound_data: dict | SMSSendRequestData = None, + inbound_files: dict = None, + **kwargs +): + + """ + Use this API when someone wants to send one or more SMS 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. + :param kwargs: Any number of extra inputs supplied by the decorators. + :return: A standard response structure. + """ + + # ┏┓ ┓ ┏┓┓ ┓ + # ┣┫┓┏╋┣┓ ┃ ┣┓┏┓┏┃┏ + # ┛┗┗┻┗┛┗ ┗┛┛┗┗ ┗┛┗ + + # Either the session must be valid, or + # the IP address requesting the service must be whitelisted: + if ( + kwargs.get("session_info") is None and + inbound_headers["Remote-IP"] not in current_app.whitelisted_ips + ): + return ResponseModel( + status_code = StatusCodes.FAILED, + http_code = HttpCodes.UNAUTHORIZED, + message = "Invalid session and/or bad IP addr." + ) + + # Get the token from the token key: + auth_token = await current_app.sms_controller.get_token_from_key( + mongo_data_conn = current_app.data_mongo, + token_key = inbound_data.tokenKey + ) + if auth_token is None: return ResponseModel( + status_code = StatusCodes.FAILED, + http_code = HttpCodes.UNAUTHORIZED, + message = f"No such token key." + ) + + # ┏┓ ┓ ┏┳┓┓ ┏┓┳┳┓┏┓ + # ┗┓┏┓┏┓┏┫ ┃ ┣┓┏┓ ┗┓┃┃┃┗┓ + # ┗┛┗ ┛┗┗┻ ┻ ┛┗┗ ┗┛┛ ┗┗┛ + + sending_results = await send_sms_messages( + mongo_data_conn = current_app.data_mongo, + auth_token = auth_token, + messages = inbound_data.message, + tags = inbound_data.tags + ) + + # ┳┓ + # ┣┫┏┓┏┏┓┏┓┏┓┏┏┓ + # ┛┗┗ ┛┣┛┗┛┛┗┛┗ + # ┛ + + # Done here: + success = True if sending_results.successCount else False + return ResponseModel( + status_code = StatusCodes.OK if success else StatusCodes.FAILED, + http_code = HttpCodes.SUCCESS if success else HttpCodes.INTERNAL_SERVER_ERROR, + data = { + "successCount": sending_results.successCount, + "failureCount": sending_results.failureCount, + "totalCount": sending_results.totalCount, + }, + message = sending_results.message + ) + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/api/blueprints/message/chat/tags.py b/api/blueprints/message/chat/tags.py new file mode 100644 index 0000000..803418b --- /dev/null +++ b/api/blueprints/message/chat/tags.py @@ -0,0 +1,226 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Thursday, 19th Dec., 2024 + + OBJECTIVE: + + To update tags on SMS messages. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + + NOTES: + + N/A + +""" + + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# For using Quart: +from quart import Blueprint, current_app, request + +# My utils: +from utils_v2.string import json +from utils_v2.api.codes import StatusCodes, HttpCodes +from utils_v2.api.response import ResponseModel +from utils_v2.api.async_quart import ( + set_api_version, + read_input, + get_session_info, + log_request_to_mongo, + log_chain_to_mongo, + should_not_be_under_maintenance, + only_whitelisted_ips, + limit_rate, + validate_input, + handle_cancelled_request +) + +# Common: +from shared import constants + +# Data Models: +from models.core.user import CoreUserInfoModel +from models.api.message.sms.tags import SMSUpdateTagsRequestHeaders, SMSUpdateTagsRequestData + +# Helpers: +from api.helpers.user import token_check + +# To work with MongoDB: +from bson import ObjectId + +# For asynchronous activities: +import asyncio + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# Related to Quart: +sms_update_tags_bp = Blueprint("sms_upd_tags", __name__) + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +@sms_update_tags_bp.record_once +def init(blueprint_setup_state): + + # This gets called when the blueprint is registered. + # Consider this to be a one-time setup for the whole blueprint: + pass + + +# --------------------------------------------------------------------------------------------------------------------- + + +@sms_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") +@log_request_to_mongo( + attr_name = "logs_mongo", + project = constants.PROJECT_NAME, + log_type = constants.MODULE_NAME, + operation = "smsUpdTagsApi", + log_input = True, + 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: SMSUpdateTagsRequestHeaders(**x).model_dump(), + data_validator = lambda x: SMSUpdateTagsRequestData(**x) +) +@handle_cancelled_request() +async def update_sms_tags( + inbound_headers: dict | SMSUpdateTagsRequestHeaders = None, + inbound_data: dict | SMSUpdateTagsRequestData = None, + inbound_files: dict = None, + **kwargs +): + + """ + Use this APi when the user wants to update the tags on one SMS. + :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: + return ResponseModel( + status_code = StatusCodes.FAILED, + http_code = HttpCodes.UNAUTHORIZED + ) + + # ┏┓ ┓ ┳┳┓ + # ┣ ┏┓╋┏┣┓ ┃┃┃┏┓┏┏┏┓┏┓┏┓ + # ┻ ┗ ┗┗┛┗ ┛ ┗┗ ┛┛┗┻┗┫┗ + # ┛ + + # Get the message: + message = await current_app.sms_controller.get_message( + mongo_data_conn = current_app.data_mongo, + message_id = inbound_data.messageId + ) + + # ┏┓ ┓ • ┏┓┓ ┓ + # ┃┃┓┏┏┏┓┏┓┏┓┏┣┓┓┏┓ ┃ ┣┓┏┓┏┃┏ + # ┗┛┗┻┛┛┗┗ ┛ ┛┛┗┗┣┛ ┗┛┛┗┗ ┗┛┗ + # ┛ + + # Check if the token(s) belong to the user claiming ownership: + if not await token_check.is_authorized( + mongo_data_conn = current_app.data_mongo, + user_info = CoreUserInfoModel(**kwargs["session_info"]), + token_ids = [message.tokenId] + ): return ResponseModel( + status_code = StatusCodes.FAILED, + http_code = HttpCodes.UNAUTHORIZED, + message = "User doesn't have rights to this message." + ) + + # ┳┳ ┓ ┳┳┓ + # ┃┃┏┓┏┫┏┓╋┏┓ ┃┃┃┏┓┏┏┏┓┏┓┏┓ + # ┗┛┣┛┗┻┗┻┗┗ ┛ ┗┗ ┛┛┗┻┗┫┗ + # ┛ ┛ + + # Update the message: + success = await current_app.sms_controller.update_sms_tags( + mongo_data_conn = current_app.data_mongo, + message_id = inbound_data.messageId, + unset_tags = inbound_data.unsetTags, + set_tags = inbound_data.setTags + ) + + # ┳┓ + # ┣┫┏┓┏┏┓┏┓┏┓┏┏┓ + # ┛┗┗ ┛┣┛┗┛┛┗┛┗ + # ┛ + + # Done here: + return ResponseModel( + status_code = StatusCodes.OK if success else StatusCodes.FAILED, + http_code = HttpCodes.SUCCESS if success else HttpCodes.INTERNAL_SERVER_ERROR + ) + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/models/api/message/chat/list.py b/models/api/message/chat/list.py new file mode 100644 index 0000000..04bd4a5 --- /dev/null +++ b/models/api/message/chat/list.py @@ -0,0 +1,157 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Tuesday, 3rd Dec., 2024. + + OBJECTIVE: + + To provide a structure to query the full payload of an email. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + +""" + + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# For making data behaviour_models: +from pydantic import BaseModel, Field, field_validator, PastDatetime +from typing import Optional, Literal, List, Any + +# My utils: +from utils_v2.string import regex +from utils_v2.date_time import date_time + +# To work with date and time: +import datetime + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# RegEx Patterns: +REGEX_SESSION_TOKEN = r"^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$" + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +class SMSListRequestHeaders(BaseModel): + + sessionToken: str = Field( + description = "the session token of the user who is requesting the service", + pattern = REGEX_SESSION_TOKEN, + frozen = True, + alias = "X-Session-Token" + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "allow" + + def model_dump(self, *args, **kwargs): + return super().model_dump(*args, by_alias = True, **kwargs) + + +# --------------------------------------------------------------------------------------------------------------------- + + +class SMSListRequestData(BaseModel): + + tokenKeys: str | List[str] = Field( + description = "the token identifier(s) that tell you which auth-tokens were used for fetching those messages", + frozen = True, + ) + + count: int = Field( + description = "the no. of mails to list", + default = 25, + ge = 1, + le = 500, + frozen = True + ) + + fromCount: int = Field( + 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", + default = None + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + # ┓┏ ┓• ┓ • + # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ + # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ + + @field_validator("tokenKeys", "tags", mode = "before") + def ensure_unique_list(cls, value): + if not isinstance(value, list): value = [value] + value = list(set(value)) + return value + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/models/api/message/chat/send.py b/models/api/message/chat/send.py new file mode 100644 index 0000000..c60b213 --- /dev/null +++ b/models/api/message/chat/send.py @@ -0,0 +1,160 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Monday, 9th Dec., 2024. + + OBJECTIVE: + + To provide a structure to receive API calls to send SMS messages from various third-party clients. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + +""" + + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# For making data behaviour_models: +from pydantic import BaseModel, Field, field_validator, PastDatetime +from typing import Optional, Literal, Union, List, Any + +# My utils: +from utils_v2.string import regex +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 +) + +# To work with date and time: +import datetime + +# To work with MongoDB: +from bson.objectid import ObjectId + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# RegEx Patterns: +REGEX_SESSION_TOKEN = r"^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$" + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +class SMSSendRequestHeaders(BaseModel): + + sessionToken: str | None = Field( + description = "the session token of the user who is requesting the service", + pattern = REGEX_SESSION_TOKEN, + frozen = True, + default = None, + alias = "X-Session-Token" + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "allow" + + def model_dump(self, *args, **kwargs): + return super().model_dump(*args, by_alias = True, **kwargs) + + +# --------------------------------------------------------------------------------------------------------------------- + + +class SMSSendRequestData(BaseModel): + + tokenKey: ObjectId + message: List[NimbusSMSIndiaMessage] | List[SavvyBulkSMSKenyaMessage] + tags: List[Any] | None = Field(default = None, validate_default = True) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + arbitrary_types_allowed = True + + # ┓┏ ┓• ┓ • + # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ + # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ + + @field_validator("tokenKey", mode = "before") + def parse_oid(cls, value): + try: value = ObjectId(value) + except: pass + return value + + @field_validator("message", mode = "before") + def ensure_list(cls, value): + if not isinstance(value, list): value = [value] + return value + + @field_validator("tags", mode = "after") + def null_to_list(cls, value): + return [] if value is None else value + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/models/api/message/chat/tags.py b/models/api/message/chat/tags.py new file mode 100644 index 0000000..655a276 --- /dev/null +++ b/models/api/message/chat/tags.py @@ -0,0 +1,139 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Thursday, 19th Dec., 2024. + + OBJECTIVE: + + To provide a structure to work with the tags on SMS messages. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + +""" + + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# For making data behaviour_models: +from pydantic import BaseModel, Field, field_validator, PastDatetime +from typing import Optional, Literal, List, Any + +# My utils: +from utils_v2.string import regex +from utils_v2.date_time import date_time + +# To work with date and time: +import datetime + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# RegEx Patterns: +REGEX_SESSION_TOKEN = r"^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$" + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +class SMSUpdateTagsRequestHeaders(BaseModel): + + sessionToken: str = Field( + description = "the session token of the user who is requesting the service", + pattern = REGEX_SESSION_TOKEN, + frozen = True, + alias = "X-Session-Token" + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "allow" + + def model_dump(self, *args, **kwargs): + return super().model_dump(*args, by_alias = True, **kwargs) + + +# --------------------------------------------------------------------------------------------------------------------- + + +class SMSUpdateTagsRequestData(BaseModel): + + messageId: str = Field( + description = "the mail identifier (Mongo ObjectId) of the document that holds the mail", + frozen = True + ) + + unsetTags: List[Any] | None = Field( + description = "the list of tags to remove from the mail", + frozen = True, + default = None + ) + + setTags: List[Any] | None = Field( + description = "the list of tags to add to the mail", + frozen = True, + default = None + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/models/message/chat/send.py b/models/message/chat/send.py new file mode 100644 index 0000000..f002354 --- /dev/null +++ b/models/message/chat/send.py @@ -0,0 +1,239 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Monday, 9th Dec., 2024. + + OBJECTIVE: + + To provide a structure to receive API calls to send SMS messages from various third-party clients. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + +""" + + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# For making data behaviour_models: +from pydantic import BaseModel, Field, field_validator, PastDatetime +from typing import Optional, Literal, Union, List, Any + +# My utils: +from utils_v2.string import regex +from utils_v2.date_time import date_time + +# Models: +from models.core.message import CoreMessageModel +from utils_v2.sms.models.sms_message import SentSMSMessageModel + +# To work with date and time: +import datetime + +# To work with MongoDB: +from bson.objectid import ObjectId + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# RegEx Patterns: +REGEX_SESSION_TOKEN = r"^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$" + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +class NimbusSMSIndiaMessage(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}", + frozen = True + ) + + text: 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 + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + # ┓┏ ┓• ┓ • + # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ + # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ + + @field_validator("recipientNo", mode = "before") + def validate_contact_nos(cls, value): + value = regex.replace(text = str(value), pattern = r"[^\d]", substitute_text = "") + if len(value) > 10: value = regex.replace(text = str(value), pattern = r"^(91)", substitute_text = "") + value = regex.find_first(text = str(value), pattern = r"^[\d]{10}") + return value + + +# --------------------------------------------------------------------------------------------------------------------- + + +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): + + 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: 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" + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + from utils_v2.string import json + + my_request = SMSSendRequestData( + tokenId = "670f580d7cda4ebc1adc3444", + message = NimbusSMSIndiaMessage( + recipientNo = "+.9.1 ---> 93261 3642fgnn193261", + text = "Hello, Nimbus!", + templateId = "12345678" + ) + ) + + print("NIMBUS SMS API MODEL:", json.to_string(my_request.model_dump(), default = str)) diff --git a/utils_v2/whatsapp/nimbus/controllers/async_nimbus_whatsapp.py b/utils_v2/whatsapp/nimbus/controllers/async_nimbus_whatsapp.py index 11b4ace..74b1d3f 100644 --- a/utils_v2/whatsapp/nimbus/controllers/async_nimbus_whatsapp.py +++ b/utils_v2/whatsapp/nimbus/controllers/async_nimbus_whatsapp.py @@ -196,30 +196,6 @@ class AsyncNimbusWhatsapp: response_json = api_response.json() ) - # print("HTTP CODE:", api_response.status_code) - # print("RAW CONTENT:", api_response.content) - # api_json = api_response.json() - # print("API JSON:", api_json) - # print("API JSON:", json.to_string(api_json, default=str)) - - # # For a successful API call: - # if api_response.status_code in [200]: - # api_json = api_response.json() - # print(api_json) - # # summary.rawResponse = api_json - # # print (summary.rawResponse) - # - # first_response = api_json - # first_desc = first_response.get("status", "N/A").lower().strip() - # summary.success = True if first_desc == "success" else False - # if summary.success: - # summary.messageId = first_response.get("requestid") - # else: - # summary.message = first_response.get("status") - # print (summary) - # # For any other code that indicates some form of failure: - # else: summary.rawResponse = api_response.content.decode() - # If something goes wrong along the way: except Exception as exception: self.__printer(exception) diff --git a/utils_v2/whatsapp/nimbus/models/send_message_response.py b/utils_v2/whatsapp/nimbus/models/send_message_response.py index c3260a3..4ae4fff 100644 --- a/utils_v2/whatsapp/nimbus/models/send_message_response.py +++ b/utils_v2/whatsapp/nimbus/models/send_message_response.py @@ -215,7 +215,11 @@ class NimbusWhatsappSendMessageResponse(BaseModel): if self.image1Url: api_input["img2"] = self.image1Url # Add the scheduled delivery time: - if self.scheduleTs: api_input["scheduleon"] = self.scheduleTs + if self.scheduleTs: + api_input["scheduleon"] = date_time.to_timezone( + datetime_object = self.scheduleTs, + timezone = date_time.TIMEZONE_IST + ).strftime("%Y%m%d%H%M") # Done here: return api_input