(20241209) Many changes to the SMS section.

This commit is contained in:
2024-12-09 19:38:47 +05:30
parent 725d68f195
commit d10b156c0b
14 changed files with 1406 additions and 250 deletions
+1 -1
View File
@@ -606,10 +606,10 @@ def get_session_info(
if hasattr(current_app, "printer"): getattr(current_app, "printer")(exception)
# We note down whatever we got:
session_info = summarize_variable(session_info, expand = True, sensitive_keys = sensitive_keys)
if session_info and isinstance(get, str):
for subkey in get.split("."):
session_info = session_info.get(subkey, {}) if isinstance(session_info, dict) else {}
session_info = summarize_variable(session_info, expand = True, sensitive_keys = sensitive_keys)
kwargs["session_info"] = session_info
# If no session info was found, but it was mandatory:
+87 -36
View File
@@ -55,8 +55,9 @@ from bs4 import BeautifulSoup
# To work with datatypes:
from typing import Any, Dict
# To work with base-64 encoding:
# To work with various encodings:
import base64
import quopri
# *****************************************************************************************************************
@@ -86,6 +87,51 @@ import base64
# *****************************************************************************************************************
def from_quoted_printable(text: str) -> str:
decoded_text = ""
decoded_bytes = quopri.decodestring(text)
for encoding in ["utf-8", "utf-16", "utf-32", "latin1"]:
try: text = decoded_bytes.decode(encoding)
except: text = ""
if text.find("From") >= 0:
decoded_text = text
break
return decoded_text
# ---------------------------------------------------------------------------------------------------------------------
def find_in_raw_mail(
raw_mail: str,
text: str
) -> int:
# We first treat it as un-encoded text:
offset = raw_mail.find(text)
if offset >= 0: return offset
# Then we try Base64 encoding:
offset = raw_mail.find(base64.b64encode(text.encode("utf-8")).decode("utf-8"))
if offset >= 0: return offset
# then we try Quoted-Printable encoding:
offset = from_quoted_printable(raw_mail).find(text)
print("MAIL:")
print(raw_mail)
print("\n\n\n---\n\n\n")
print("TEXT:")
print(quopri.encodestring(text.encode("utf-8")).decode("utf-8"))
if offset >= 0: return offset
# Done here, even if nothing worked:
return offset
# ---------------------------------------------------------------------------------------------------------------------
def parse(raw_mail: str | bytes) -> Dict[str, Any]:
"""
@@ -102,6 +148,9 @@ def parse(raw_mail: str | bytes) -> Dict[str, Any]:
if isinstance(raw_mail, str): parsed_mail = mailparser.parse_from_string(raw_mail)
else: parsed_mail = mailparser.parse_from_bytes(raw_mail)
# print(parsed_mail.mail_json)
# return
# Format the attachments:
message_attachments = [
{
@@ -117,46 +166,43 @@ def parse(raw_mail: str | bytes) -> Dict[str, Any]:
} for attachment in parsed_mail.attachments
]
# Figure out which entity (text and HTML) came in which sequence.
# The library doesn't give us any sequence info so we do some custom string processing here to figure out the order
# in which to render the contents of the page.
parts = [
{
"partNo": None,
"offset": max(
parsed_mail.message_as_string.find(t),
parsed_mail.message_as_string.find(base64.b64encode(t.encode()).decode())
),
"type": "text/plain",
"data": t
} for t in parsed_mail.text_plain
]
parts = parts + [
{
"partNo": None,
"offset": max(
parsed_mail.message_as_string.find(h),
parsed_mail.message_as_string.find(base64.b64encode(h.encode()).decode())
),
"type": "text/html",
"data": h
} for h in parsed_mail.text_html
]
parts = sorted(parts, key = lambda x: x["offset"])
for i, p in enumerate(parts): p["partNo"] = i
# print("PRINTING PARTS")
# print("LIBRARY PARTS:", type(parsed_mail))
# # Figure out which entity (text and HTML) came in which sequence.
# # The library doesn't give us any sequence info so we do some custom string processing here to figure out the order
# # in which to render the contents of the page.
# parts = [
# # {
# # "partNo": None,
# # "offset": max(
# # parsed_mail.message_as_string.find(t),
# # parsed_mail.message_as_string.find(base64.b64encode(t.encode()).decode())
# # ),
# # "type": "text/plain",
# # "data": t
# # } for t in parsed_mail.text_plain
# ]
# parts = parts + [
# {
# "partNo": None,
# "offset": find_in_raw_mail(raw_mail = parsed_mail.message_as_string, text = h),
# "type": "text/html",
# "data": h
# } for h in parsed_mail.text_html
# ]
# parts = sorted(parts, key = lambda x: x["offset"])
# for i, p in enumerate(parts): p["partNo"] = i
# Get the unformatted text from everything in the mail:
unformatted_text = []
for p in parts:
if p["type"] == "text/html":
html_parser = BeautifulSoup(p["data"], "html.parser")
unformatted_text.append(html_parser.get_text())
else: unformatted_text.append(p["data"])
for p in parsed_mail.text_html:
html_parser = BeautifulSoup(p, "html.parser")
unformatted_text.append(html_parser.get_text())
# Put everything together:
return {
"ts": date_time.to_timezone(parsed_mail.date, timezone = date_time.TIMEZONE_UTC),
"readTs": date_time.get_current_utc_date_time(as_string = False),
"headers": parsed_mail.headers,
"from": [{"name": _[0] or _[1], "email": _[1]} for _ in parsed_mail.headers["From"]],
"to": [{"name": _[0] or _[1], "email": _[1]} for _ in parsed_mail.headers["To"]],
@@ -165,7 +211,7 @@ def parse(raw_mail: str | bytes) -> Dict[str, Any]:
"subject": parsed_mail.headers["Subject"],
"text": parsed_mail.text_plain,
"html": parsed_mail.text_html,
"parts": parts,
# "parts": parts,
"unformattedText": "\n".join(unformatted_text),
"attachments": message_attachments,
"isInbox": None
@@ -181,4 +227,9 @@ def parse(raw_mail: str | bytes) -> Dict[str, Any]:
if __name__ == "__main__":
pass
from utils_v2.system import files
mail_string_raw = files.read_file(r"/home/developer/Downloads/raw_mail.txt")
parse_results = parse(mail_string_raw)
print(json.to_string(parse_results, default = str))
@@ -43,6 +43,9 @@ sys.path.append("..")
# To make API Calls:
import httpx
# Data models:
from utils_v2.sms.models.data.sms_message import SentSMSMessageModel
# For debugging:
from icecream import IceCreamDebugger
@@ -172,12 +175,12 @@ class AsyncNimbusSMS:
async def send_sms(
self,
template_id,
recipient_number,
message,
message_type = MESSAGE_TYPE_REGULAR,
flash = MESSAGE_TYPE_NOT_FLASH
):
template_id: str,
recipient_number: str,
message: str,
message_type: int = MESSAGE_TYPE_REGULAR,
flash: int = MESSAGE_TYPE_NOT_FLASH
) -> SentSMSMessageModel:
"""
Sends one SMS through Nimbus IT's system. The text of the message must match the template that had been
@@ -195,17 +198,14 @@ class AsyncNimbusSMS:
"""
# Construct the basic structure of the response of this method:
summary = {
"success": False,
"info": None,
"sender": self.__sender_id,
"recipient": recipient_number,
"message": message,
"length": len(message),
"template_id": template_id,
"raw": None,
"isFlash": True if flash else False
}
summary = SentSMSMessageModel(
sender = {"senderId": self.__sender_id},
recipient = {"recipientNo": recipient_number},
text = message,
length = len(message),
isFlash = True if flash else False,
metadata = {"templateId": template_id}
)
try:
@@ -231,18 +231,13 @@ class AsyncNimbusSMS:
# For a successful API call:
if response.status_code == 200:
response_json = response.json()
summary["success"] = True if response_json.get("STATUS", "ERROR").lower() in ["ok"] else False
summary["info"] = response_json.get("RESPONSE", {}).get("INFO")
summary["raw"] = {
"http_code": response.status_code,
"response": response_json,
}
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")
# For any other code that indicates some form of failure:
else: summary["raw"] = {
"http_code": response.status_code,
"response": response.content.decode()
}
else: summary.rawResponse = response.content.decode()
except Exception as exception:
self.__printer(exception)
@@ -263,21 +258,21 @@ if __name__ == "__main__":
async def main():
sender = AsyncNimbusSMS(
entity_id = "<your_entity_id>",
sender_id = "<your_sender_id>",
user_id = "<your_nimbus_user_id>",
api_key = "<your_nimbus_api_key>"
client = AsyncNimbusSMS(
entity_id = "1701172465456946915",
sender_id = "TCAOFF",
user_id = "210844",
api_key = "92UnZwiiY7zps"
)
response = await sender.send_sms(
template_id = "<your_sms_template_id>",
recipient_number = "<the_number_you_want_to_send_the_message_to>",
message = "<your_sms_message>"
response = await client.send_sms(
template_id = "1707172474709021546",
recipient_number = "987039115511",
message = "OTP for The CA Office registration request is 583920. Please enter this to verify your identity and proceed with the registration request. - TCAOFF"
)
print("SMS API RESPONSE:", response)
my_balance = await sender.get_balance()
my_balance = await client.get_balance()
print("REMAINING BALANCE:", my_balance)
@@ -42,6 +42,9 @@ sys.path.append("..")
# To make API Calls:
import httpx
# Data models:
from utils_v2.sms.models.data.sms_message import SentSMSMessageModel
# For debugging:
from icecream import IceCreamDebugger
@@ -149,18 +152,19 @@ class AsyncSavvyBulkSMS:
"""
# Construct the basic structure of the response of this method:
summary = {
"success": False,
"info": None,
"sender": {
summary = SentSMSMessageModel(
sender = {
"partnerId": self.__partner_id,
"shortCode": self.__short_code
},
"recipient": recipient_number,
"message": message,
"length": len(message),
"raw": None,
}
recipient = {
"recipientNo": recipient_number
},
text = message,
length = len(message),
isFlash = False,
metadata = None
)
try:
@@ -179,18 +183,15 @@ class AsyncSavvyBulkSMS:
# For a successful API call:
if api_response.status_code in [200]:
api_json = api_response.json()
first_desc = api_json.get("responses", [{}])[0].get("response-description", "N/A").lower().strip()
summary["success"] = True if first_desc == "success" else False
summary["raw"] = {
"http_code": api_response.status_code,
"response": api_json,
}
summary.rawResponse = api_json
first_response = api_json.get("responses", [{}])[0]
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")
# For any other code that indicates some form of failure:
else: summary["raw"] = {
"http_code": api_response.status_code,
"response": api_response.content.decode()
}
else: summary.rawResponse = api_response.content.decode()
# If something goes wrong along the way:
except Exception as exception:
@@ -214,14 +215,14 @@ if __name__ == "__main__":
async def main():
sender = AsyncSavvyBulkSMS(
api_key = "<your_api_key>",
partner_id = "<your_partner_id>",
short_code = "<your_short_code>"
api_key = "19250bdd74050f7cec980c71e75f851a",
partner_id = "7462",
short_code = "HTL TV-NET"
)
response = await sender.send_sms(
recipient_number = "<target_recipient>",
message = "<message_for_recipient>"
recipient_number = "254748877373 123",
message = "Test 123"
)
print("SMS API RESPONSE:", response)
+157
View File
@@ -0,0 +1,157 @@
"""
AUTHOR:
Khushal P Soonderji
DATE:
Monday, 9th Dec., 2024.
OBJECTIVE:
To define how messages will be stored in the database.
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, AwareDatetime
from typing import Optional, Literal, Union, Any
# My utils:
from utils_v2.string import regex
from utils_v2.date_time import date_time
# To work with MongoDB:
from bson.objectid import ObjectId
# To work with date and time:
import datetime
# *****************************************************************************************************************
# ***** ****
# *** MACROS / ONE-TIME INIT ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** VARIABLES ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** FUNCTIONS ***
# ***** ****
# *****************************************************************************************************************
class SentSMSMessageModel(BaseModel):
ts: AwareDatetime = Field(
description = "the time (utc) at which this message was sent by the sender",
frozen = True,
default_factory = lambda: date_time.get_current_utc_date_time(as_string = False)
)
sender: dict = Field(
description = "the sender of this message",
frozen = True
)
recipient: dict = Field(
description = "the recipient of this message",
frozen = True
)
text: str = Field(
description = "the actual text that was sent",
frozen = True
)
length: int = Field(
description = "the no. of chars in this message",
frozen = True
)
isFlash: bool = Field(
description = "to know whether this message is a flash message or a regular message",
frozen = True
)
success: bool = Field(
description = "to know whether this message was sent successfully or not",
default = False
)
brief: str | None = Field(
description = "a brief message about what happened; useful when something goes wrong",
default = None
)
metadata: dict | None = Field(
description = "any extra data about this message",
frozen = True
)
rawResponse: Any | None = Field(
description = "the raw response from the third-party client",
default = None
)
messageId: int | str | None = Field(
description = "how the client recognizes this message",
frozen = False,
default = None
)
# ┏┓ ┏•
# ┃ ┏┓┏┓╋┓┏┓
# ┗┛┗┛┛┗┛┗┗┫
# ┛
class Config:
extra = "allow"
# *****************************************************************************************************************
# ***** ****
# *** MAIN PROGRAM ***
# ***** ****
# *****************************************************************************************************************
if __name__ == "__main__":
pass