(20241213) SMS Auth reworked.

This commit is contained in:
2024-12-13 18:28:45 +05:30
parent 4c258b110a
commit 4f5668c036
9 changed files with 476 additions and 47 deletions
+8 -29
View File
@@ -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,7 +193,7 @@ 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(
@@ -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
}
)
+9 -9
View File
@@ -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"]
# )
# ┳┓
# ┣┫┏┓┏┏┓┏┓┏┓┏┏┓
+1 -1
View File
@@ -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
}
)
+4 -2
View File
@@ -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()
# ┏┓ ┓ ┏┓┓•
# ┃ ┏┓┏┓┏┓┏┓┏╋┏┓┏┓┏ ┏┓┏┓┏┫ ┃ ┃┓┏┓┏┓╋┏
+371
View File
@@ -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"])))
+79 -2
View File
@@ -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"
# *****************************************************************************************************************
# ***** ****
@@ -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()
@@ -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()
+1 -1
View File
@@ -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
)