(20241209) Many changes to the SMS section.
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
|
||||
+32
-37
@@ -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)
|
||||
|
||||
|
||||
+25
-24
@@ -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)
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user