(20250116) Started upgrading the mail module (to eventually work on cron).
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
"""
|
||||
|
||||
AUTHOR:
|
||||
|
||||
Khushal P Soonderji
|
||||
|
||||
DATE:
|
||||
|
||||
Friday, 6th Dec., 2024
|
||||
|
||||
OBJECTIVE:
|
||||
|
||||
To register a telegram bot with our system and send a request to Telegram to set it up the webhook.
|
||||
|
||||
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, render_template
|
||||
|
||||
# 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
|
||||
)
|
||||
|
||||
# Data Models:
|
||||
from models.api.message.chat.auth import ChatAuthRequestHeaders, ChatAuthRequestData
|
||||
from models.core.auth_token import CoreAuthTokenModel
|
||||
|
||||
# Common:
|
||||
from shared import constants
|
||||
|
||||
# For asynchronous activities:
|
||||
import asyncio
|
||||
|
||||
# For random choices:
|
||||
import random
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MACROS / ONE-TIME INIT ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# Related to Quart:
|
||||
chat_auth_bp = Blueprint("chat_auth_bp", __name__)
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** VARIABLES ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# --- Nothing Yet
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** FUNCTIONS ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
@chat_auth_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 set_telegram_webhook(
|
||||
bot_token: str,
|
||||
webhook_url: str
|
||||
) -> dict:
|
||||
|
||||
"""
|
||||
To set a webhook to receive messages via a bot.
|
||||
:param bot_token: The token granted by BotFather when creating the bot.
|
||||
:param webhook_url: Your endpoint where you will receive messages that people send to your bot.
|
||||
:return: The JSON response from Telegram.
|
||||
"""
|
||||
|
||||
# Register the webhook with Telegram:
|
||||
api_response = await current_app.http_client.post(
|
||||
url = f"https://api.telegram.org/bot{bot_token}/setWebhook",
|
||||
data = {"url": webhook_url}
|
||||
)
|
||||
|
||||
# Done here:
|
||||
return api_response.json()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def get_telegram_bot_info(
|
||||
bot_token: str,
|
||||
) -> dict | None:
|
||||
|
||||
"""
|
||||
To get a bot's basic info.
|
||||
:param bot_token: The token granted by BotFather when creating the bot.
|
||||
:return: The JSON response from Telegram.
|
||||
"""
|
||||
|
||||
# Start by assuming failure:
|
||||
bot_info = None
|
||||
|
||||
# Register the webhook with Telegram:
|
||||
api_response = await current_app.http_client.get(
|
||||
url = f"https://api.telegram.org/bot{bot_token}/getMe",
|
||||
)
|
||||
|
||||
# If the call succeeds:
|
||||
if api_response.status_code in [200]:
|
||||
if api_response.json()["ok"]:
|
||||
api_result = api_response.json()["result"]
|
||||
bot_info = {
|
||||
"id": api_result["id"],
|
||||
"displayName": api_result["first_name"],
|
||||
"username": api_result["username"],
|
||||
}
|
||||
|
||||
# Done here:
|
||||
return bot_info
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@chat_auth_bp.route("/auth", 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 = "chatAuthApi",
|
||||
log_input = True,
|
||||
log_output = True,
|
||||
sensitive_keys = None
|
||||
)
|
||||
@log_chain_to_mongo(attr_name = "logs_mongo")
|
||||
@should_not_be_under_maintenance(attr_name = "is_under_maintenance")
|
||||
@validate_input(
|
||||
header_validator = lambda x: ChatAuthRequestHeaders(**x).model_dump(),
|
||||
data_validator = lambda x: ChatAuthRequestData(**x)
|
||||
)
|
||||
@handle_cancelled_request()
|
||||
async def callback_test(
|
||||
inbound_headers: dict | ChatAuthRequestHeaders = None,
|
||||
inbound_data: dict | ChatAuthRequestData = None,
|
||||
inbound_files: dict = None,
|
||||
**kwargs
|
||||
):
|
||||
|
||||
"""
|
||||
Use this when a user wants to register a third-party chat client with your service.
|
||||
: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
|
||||
)
|
||||
|
||||
# Start by assuming failure:
|
||||
success = False
|
||||
|
||||
# ┏┓ ┓ ┏┓ ┏┓ ┳┓• ┓
|
||||
# ┣ ┏┓┏┓ ┃┃┃┣┓┏┓╋┏┣┫┏┓┏┓━━┃┃┓┏┳┓┣┓┓┏┏
|
||||
# ┻ ┗┛┛ ┗┻┛┛┗┗┻┗┛┛┗┣┛┣┛ ┛┗┗┛┗┗┗┛┗┻┛
|
||||
# ┛ ┛
|
||||
|
||||
if inbound_data.chatClient == "whatsappNimbus":
|
||||
success = await current_app.whatsapp_nimbus_controller.set_token_direct(
|
||||
sql_conn = current_app.sql_writer,
|
||||
mongo_data_conn = current_app.data_mongo,
|
||||
auth_token = CoreAuthTokenModel(
|
||||
serviceType = "chat",
|
||||
client = inbound_data.chatClient,
|
||||
authType = "auth",
|
||||
auth = inbound_data.auth.model_dump(),
|
||||
user = kwargs.get("session_info"),
|
||||
clientUserId = {
|
||||
"senderId": inbound_data.auth.senderId
|
||||
},
|
||||
status = "active",
|
||||
syncFreq = 60
|
||||
),
|
||||
token_notes = {
|
||||
"apiKey": inbound_data.auth.apiKey,
|
||||
"senderId": inbound_data.auth.senderId
|
||||
},
|
||||
display_name = inbound_data.auth.senderId,
|
||||
session_token = inbound_headers["X-Session-Token"]
|
||||
)
|
||||
|
||||
# ┳┓
|
||||
# ┣┫┏┓┏┏┓┏┓┏┓┏┏┓
|
||||
# ┛┗┗ ┛┣┛┗┛┛┗┛┗
|
||||
# ┛
|
||||
|
||||
# Done here:
|
||||
return ResponseModel(
|
||||
status_code = StatusCodes.OK if success else StatusCodes.FAILED,
|
||||
http_code = HttpCodes.SUCCESS if success else HttpCodes.INTERNAL_SERVER_ERROR,
|
||||
data = {
|
||||
"client": inbound_data.chatClient,
|
||||
"authorized": success
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MAIN PROGRAM ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,196 @@
|
||||
"""
|
||||
|
||||
AUTHOR:
|
||||
|
||||
Khushal P Soonderji
|
||||
|
||||
DATE:
|
||||
|
||||
Friday, 6th Dec., 2024
|
||||
|
||||
OBJECTIVE:
|
||||
|
||||
To register a telegram bot with our system and send a request to Telegram to set it up the webhook.
|
||||
|
||||
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, render_template
|
||||
|
||||
# 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
|
||||
)
|
||||
|
||||
# Data Models:
|
||||
from models.data.api.chat.auth import ChatAuthRequestHeaders, ChatAuthRequestData
|
||||
|
||||
# Common:
|
||||
from shared import constants
|
||||
|
||||
# For asynchronous activities:
|
||||
import asyncio
|
||||
|
||||
# For random choices:
|
||||
import random
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MACROS / ONE-TIME INIT ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# Related to Quart:
|
||||
chat_webhook_bp = Blueprint("chat_whook_bp", __name__)
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** VARIABLES ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# --- Nothing Yet
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** FUNCTIONS ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
@chat_webhook_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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@chat_webhook_bp.route("/webhook/<token_id>", methods = ["POST", "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 = "chatWHookApi",
|
||||
log_input = True,
|
||||
log_output = True,
|
||||
sensitive_keys = None
|
||||
)
|
||||
@log_chain_to_mongo(attr_name = "logs_mongo")
|
||||
@should_not_be_under_maintenance(attr_name = "is_under_maintenance")
|
||||
# @validate_input(
|
||||
# header_validator = lambda x: ChatAuthRequestHeaders(**x).model_dump(),
|
||||
# data_validator = lambda x: ChatAuthRequestData(**x)
|
||||
# )
|
||||
@handle_cancelled_request()
|
||||
async def callback_test(
|
||||
token_id: str = None,
|
||||
inbound_headers: dict = None,
|
||||
inbound_data: dict = None,
|
||||
inbound_files: dict = None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
This URL receives messages from chatbots as webhooks.
|
||||
:param token_id: The identifier of the bot.
|
||||
: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.
|
||||
"""
|
||||
|
||||
# Construct a message:
|
||||
message = "🪝 *WEBHOOK/CALLBACK ALERT!* 🪝\n\n"
|
||||
message += f"Method: *{request.method}*\nLog Id.: `{kwargs.get('log_id')}`\n\n"
|
||||
message += "*Headers:*\n```json\n"
|
||||
message += json.to_string({k: v for k, v in request.headers.items()})
|
||||
message += "\n```\n"
|
||||
message += "*Query Args:*\n```json\n"
|
||||
message += json.to_string(request.args.to_dict())
|
||||
message += "\n```\n"
|
||||
message += "*JSON:*\n```json\n"
|
||||
message += json.to_string(await request.get_json())
|
||||
message += "\n```\n"
|
||||
message += "*Form-Data:*\n```json\n"
|
||||
message += json.to_string((await request.form).to_dict())
|
||||
message += "\n```\n"
|
||||
message += "*Form-Files:*\n```json\n"
|
||||
message += json.to_string(inbound_files, default=str)
|
||||
message += "\n```\n"
|
||||
|
||||
# Send a message on Telegram:
|
||||
api_response = await current_app.http_client.post(
|
||||
url = current_app.script_data["alerts"]["url"],
|
||||
json = {
|
||||
"message": message,
|
||||
"type": "info",
|
||||
"chatId": "1275560043" # ... KPS
|
||||
}
|
||||
)
|
||||
|
||||
# Return a success response:
|
||||
return ResponseModel(
|
||||
status_code = StatusCodes.OK,
|
||||
data = {"accepted": True}
|
||||
)
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MAIN PROGRAM ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,326 @@
|
||||
"""
|
||||
|
||||
AUTHOR:
|
||||
|
||||
Khushal P Soonderji
|
||||
|
||||
DATE:
|
||||
|
||||
Monday, 25th Nov., 2024
|
||||
|
||||
OBJECTIVE:
|
||||
|
||||
To receive callbacks (webhooks).
|
||||
|
||||
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, g, request, render_template
|
||||
|
||||
# My utils:
|
||||
from utils_v2.string import json
|
||||
from utils_v2.logging.context import AsyncLoggerContext
|
||||
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
|
||||
)
|
||||
|
||||
# GMail-related utils:
|
||||
from utils_v2.goog.controllers.gmail.gmail_client import SCOPES_GMAIL_MAIL_MANAGEMENT
|
||||
|
||||
# Data Models:
|
||||
from models.core.auth_token import CoreAuthTokenModel
|
||||
|
||||
# Common:
|
||||
from shared import constants
|
||||
|
||||
# For asynchronous activities:
|
||||
import asyncio
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MACROS / ONE-TIME INIT ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# Related to Quart:
|
||||
mail_oauth_callback_bp = Blueprint("mail_cb", __name__)
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** VARIABLES ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# --- Nothing Yet
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** FUNCTIONS ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
@mail_oauth_callback_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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@AsyncLoggerContext.log_it(
|
||||
api_version = "1.0.0",
|
||||
project = constants.PROJECT_NAME,
|
||||
log_type = constants.MODULE_NAME,
|
||||
operation = "gmailClbk",
|
||||
log_input = 2,
|
||||
log_output = 1,
|
||||
sensitive_keys = ["sessionToken", "X-Session-Token"]
|
||||
)
|
||||
async def handle_gmail_callback() -> render_template:
|
||||
|
||||
"""
|
||||
To handle the callbacks from GMail specifically. Refer to the individual comments to check what hap[pens at each
|
||||
step of the process.
|
||||
:return: A rendered template (HTML) of the final status of the authorization.
|
||||
"""
|
||||
|
||||
# Start by assuming failure:
|
||||
tokens_saved = False
|
||||
|
||||
# In case the user cancelled halfway through (on Google's screen):
|
||||
if g.inbound_data.get("error") == "access_denied": return await render_template(
|
||||
"/message/mail/oauth/oauth_cancelled_v2.html",
|
||||
mail_client = g.mail_client.title()
|
||||
)
|
||||
|
||||
# Generate the tokens from the callback. Google sends all the needed params in the callback as the URL's query
|
||||
# params. We can simply use the exact URL that was hit to generate the tokens. In Quart (and Flask) this can be
|
||||
# achieved by 'request.url' like this:
|
||||
google_tokens = await current_app.gmail_client.get_authorization_tokens(
|
||||
redirect_url = request.url,
|
||||
scopes = None
|
||||
)
|
||||
|
||||
if google_tokens:
|
||||
|
||||
# Get the e-mail id that granted authorization. We will be comparing this to the e-mail id that had been given
|
||||
# to us when the authorization was initiated. We don't mind any e-mail id being used, but we need them to be the
|
||||
# same at both ends:
|
||||
user_profile = await current_app.gmail_client.get_user_profile(tokens = google_tokens)
|
||||
if user_profile.success:
|
||||
google_tokens.email = user_profile.data["emailAddress"]
|
||||
google_tokens.displayName = user_profile.data["displayName"]
|
||||
google_tokens.displayPictureUrl = user_profile.data["displayPictureUrl"]
|
||||
|
||||
# Here's where we do the checking of the e-mails,
|
||||
# if they don't match, we reject the authorization:
|
||||
auth_token = await current_app.mail_controller.get_token_from_key(
|
||||
mongo_conn = current_app.data_mongo,
|
||||
token_key = g.inbound_data["state"]
|
||||
)
|
||||
if (
|
||||
(not auth_token) or
|
||||
auth_token.clientUserId["email"] != str(google_tokens.email)
|
||||
): return await render_template(
|
||||
"/message/mail/oauth/oauth_failure_v2.html",
|
||||
mail_client = g.mail_client.title(),
|
||||
failure_hint = (
|
||||
f"We were expecting authorization from '{auth_token.clientUserId['email']}', "
|
||||
f"but got authorization from '{google_tokens.email}' instead."
|
||||
)
|
||||
)
|
||||
|
||||
# We create standard labels that we will use:
|
||||
labels = [
|
||||
{
|
||||
"name": "TCAOFF",
|
||||
"textColor": "#434343",
|
||||
"backgroundColor": "#e7e7e7"
|
||||
},
|
||||
{
|
||||
"name": "CA-Doc",
|
||||
"textColor": "#434343",
|
||||
"backgroundColor": "#e7e7e7"
|
||||
},
|
||||
{
|
||||
"name": "CA-AI",
|
||||
"textColor": "#434343",
|
||||
"backgroundColor": "#e7e7e7"
|
||||
}
|
||||
]
|
||||
tasks = [
|
||||
current_app.gmail_client.create_label(
|
||||
tokens = google_tokens,
|
||||
label_name = label["name"],
|
||||
label_visibility = "labelShow",
|
||||
message_visibility = "show",
|
||||
label_text_color = label["textColor"],
|
||||
label_background_color = label["backgroundColor"]
|
||||
) for label in labels
|
||||
]
|
||||
client_responses = await asyncio.gather(*tasks)
|
||||
|
||||
# Add the labels to the tokens data:
|
||||
client_response = await current_app.gmail_client.list_labels(tokens = google_tokens)
|
||||
google_tokens.labels = client_response.data if client_response.success else None
|
||||
|
||||
# Now that we have passed the check,
|
||||
# we save the tokens to the database:
|
||||
auth_token.clientUserId = google_tokens.client_user_id
|
||||
auth_token.token = google_tokens.model_dump()
|
||||
auth_token.status = "active"
|
||||
tokens_saved = await current_app.mail_controller.set_token(
|
||||
db_conn = current_app.sql_writer,
|
||||
mongo_conn = current_app.data_mongo,
|
||||
session_token = g.inbound_headers.get("X-Session-Token"),
|
||||
token_key = g.inbound_data["state"],
|
||||
auth_token = auth_token
|
||||
)
|
||||
|
||||
# Return an HTML response for success:
|
||||
if tokens_saved: return await render_template(
|
||||
"/message/mail/oauth/oauth_success_v2.html",
|
||||
mail_client = g.mail_client.title()
|
||||
)
|
||||
|
||||
# Return an HTML response for failure:
|
||||
else: return await render_template(
|
||||
"/message/mail/oauth/oauth_failure_v2.html",
|
||||
mail_client = g.mail_client.title(),
|
||||
failure_hint = f"Unknown error. Please use log-id '{g.log_id}' to check with the support team."
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@mail_oauth_callback_bp.route("/callback/<mail_client>", methods = ["POST", "GET"])
|
||||
@set_api_version(api_version = "1.0.0")
|
||||
@read_input(sanitize_headers = False, sanitize_data = False)
|
||||
@log_request_to_mongo(
|
||||
attr_name = "logs_mongo",
|
||||
project = constants.PROJECT_NAME,
|
||||
log_type = constants.MODULE_NAME,
|
||||
operation = "gmailCllBckApi",
|
||||
log_input = True,
|
||||
log_output = True,
|
||||
sensitive_keys = None
|
||||
)
|
||||
@log_chain_to_mongo(attr_name = "logs_mongo")
|
||||
@should_not_be_under_maintenance(attr_name = "is_under_maintenance")
|
||||
@handle_cancelled_request()
|
||||
async def mail_auth_callback(
|
||||
mail_client: str = None,
|
||||
inbound_headers: dict = None,
|
||||
inbound_data: dict = None,
|
||||
inbound_files: dict = None,
|
||||
**kwargs
|
||||
):
|
||||
|
||||
"""
|
||||
Use this when authorizing access to someone's GMail account. This can be used to capture the authentication token.
|
||||
:param mail_client: The mail company/brand that you want the authorization from.
|
||||
: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.
|
||||
"""
|
||||
|
||||
# ┓┏ ┓┓ ┓┏ • ┓ ┓
|
||||
# ┣┫┏┓┏┓┏┫┃┏┓ ┃┃┏┓┏┓┓┏┓┣┓┃┏┓┏
|
||||
# ┛┗┗┻┛┗┗┻┗┗ ┗┛┗┻┛ ┗┗┻┗┛┗┗ ┛
|
||||
|
||||
# Here we make various variables available in the scope of the current request through 'g':
|
||||
g.log_id = kwargs.get("log_id")
|
||||
g.inbound_headers = inbound_headers
|
||||
g.inbound_data = inbound_data
|
||||
g.mail_client = mail_client
|
||||
|
||||
# ┓┏ ┓┓ ┏┓ ┓┓┓ ┓
|
||||
# ┣┫┏┓┏┓┏┫┃┏┓ ┃ ┏┓┃┃┣┓┏┓┏┃┏┏
|
||||
# ┛┗┗┻┛┗┗┻┗┗ ┗┛┗┻┗┗┗┛┗┻┗┛┗┛
|
||||
|
||||
try:
|
||||
|
||||
if mail_client == "gmail": return await handle_gmail_callback()
|
||||
|
||||
# If something goes wrong:
|
||||
except Exception as exception: return await render_template(
|
||||
"/message/mail/oauth/oauth_failure_v2.html",
|
||||
mail_client = mail_client.title(),
|
||||
failure_hint = (
|
||||
f"An internal server error occurred. "
|
||||
f"Please use log-id '{g.log_id}' to check with the support team."
|
||||
)
|
||||
)
|
||||
|
||||
# ┓┏ ┓┓ ┳ ┓• ┓ ┏┓┓•
|
||||
# ┣┫┏┓┏┓┏┫┃┏┓ ┃┏┓┓┏┏┓┃┓┏┫ ┃ ┃┓┏┓┏┓╋
|
||||
# ┛┗┗┻┛┗┗┻┗┗ ┻┛┗┗┛┗┻┗┗┗┻ ┗┛┗┗┗ ┛┗┗
|
||||
|
||||
return await render_template(
|
||||
"/message/mail/oauth/oauth_failure_v2.html",
|
||||
mail_client = mail_client.title(),
|
||||
failure_hint = (
|
||||
f"Invalid client '{mail_client}' selected. "
|
||||
f"Please use log-id '{g.log_id}' to check with the support team."
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MAIN PROGRAM ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,275 @@
|
||||
"""
|
||||
|
||||
AUTHOR:
|
||||
|
||||
Khushal P Soonderji
|
||||
|
||||
DATE:
|
||||
|
||||
Thursday, 16th Jan., 2025.
|
||||
|
||||
OBJECTIVE:
|
||||
|
||||
To receive callbacks (webhooks).
|
||||
|
||||
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, g, request, render_template
|
||||
|
||||
# My utils:
|
||||
from utils_v2.string import json
|
||||
from utils_v2.logging.context import AsyncLoggerContext
|
||||
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
|
||||
)
|
||||
|
||||
# GMail-related utils:
|
||||
from utils_v2.goog.controllers.gmail.gmail_client import SCOPES_GMAIL_MAIL_MANAGEMENT
|
||||
|
||||
# Data Models:
|
||||
from models.core.auth_token import CoreAuthTokenModel
|
||||
from models.message.mail.oauth import OAuthMailHandleCallbackResponse
|
||||
|
||||
# Common:
|
||||
from shared import constants
|
||||
|
||||
# For asynchronous activities:
|
||||
import asyncio
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MACROS / ONE-TIME INIT ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# Related to Quart:
|
||||
mail_oauth_callback_bp = Blueprint("mail_cb", __name__)
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** VARIABLES ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# --- Nothing Yet
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** FUNCTIONS ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
@mail_oauth_callback_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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@AsyncLoggerContext.log_it(
|
||||
api_version = "1.0.0",
|
||||
project = constants.PROJECT_NAME,
|
||||
log_type = constants.MODULE_NAME,
|
||||
operation = "mailOAuthClbk",
|
||||
log_input = 2,
|
||||
log_output = 1,
|
||||
sensitive_keys = ["sessionToken", "X-Session-Token"]
|
||||
)
|
||||
async def handle_mail_callback(
|
||||
client_controller,
|
||||
client_connector,
|
||||
request_url: str,
|
||||
inbound_data: dict,
|
||||
) -> OAuthMailHandleCallbackResponse:
|
||||
|
||||
"""
|
||||
This function has been kept separate only for convenience of logging.
|
||||
:param client_controller: The mail controller instance.
|
||||
:param client_connector: The instance of the third-party client to send to the mail controller.
|
||||
:param request_url: The full request URL that came in.
|
||||
:param inbound_data: The data received in the request.
|
||||
:return: The client's response.
|
||||
"""
|
||||
|
||||
return await client_controller.handle_authorization_callback(
|
||||
sql_conn = current_app.sql_writer,
|
||||
mongo_data_conn = current_app.data_mongo,
|
||||
mail_client = client_connector,
|
||||
request_url = request_url,
|
||||
inbound_data = inbound_data,
|
||||
session_token = None
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@mail_oauth_callback_bp.route("/callback/<mail_client>", methods = ["POST", "GET"])
|
||||
@set_api_version(api_version = "1.0.0")
|
||||
@read_input(sanitize_headers = False, sanitize_data = False)
|
||||
@log_request_to_mongo(
|
||||
attr_name = "logs_mongo",
|
||||
project = constants.PROJECT_NAME,
|
||||
log_type = constants.MODULE_NAME,
|
||||
operation = "mailOAuthClbkApi",
|
||||
log_input = True,
|
||||
log_output = True,
|
||||
sensitive_keys = None
|
||||
)
|
||||
@log_chain_to_mongo(attr_name = "logs_mongo")
|
||||
@should_not_be_under_maintenance(attr_name = "is_under_maintenance")
|
||||
@handle_cancelled_request()
|
||||
async def mail_auth_callback(
|
||||
mail_client: str = None,
|
||||
inbound_headers: dict = None,
|
||||
inbound_data: dict = None,
|
||||
inbound_files: dict = None,
|
||||
**kwargs
|
||||
):
|
||||
|
||||
"""
|
||||
This is the callback received when authorizing someone's mail client.
|
||||
:param mail_client: The mail company/brand that you want the authorization from.
|
||||
: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.
|
||||
"""
|
||||
|
||||
# ┳┓ ┳┳┓ •┓ ┏┓┓•
|
||||
# ┣┫┏┓┓┏╋┏┓ ╋┏┓ ┃┃┃┏┓┓┃ ┃ ┃┓┏┓┏┓╋
|
||||
# ┛┗┗┛┗┻┗┗ ┗┗┛ ┛ ┗┗┻┗┗ ┗┛┗┗┗ ┛┗┗
|
||||
|
||||
# Start by assuming failure:
|
||||
client_controller = None
|
||||
client_connector = None
|
||||
client_response = None
|
||||
exception = None
|
||||
|
||||
# Figure out the client connector:
|
||||
match mail_client:
|
||||
case "gmail": client_controller, client_connector = current_app.gmail_controller, current_app.gmail_client
|
||||
case _: client_controller, client_connector = None, None
|
||||
|
||||
# Invoke the mail client:
|
||||
if client_controller is not None and client_connector is not None:
|
||||
try: client_response = await handle_mail_callback(
|
||||
client_controller,
|
||||
client_connector,
|
||||
request_url = request.url,
|
||||
inbound_data = inbound_data
|
||||
)
|
||||
except Exception as excp: exception = excp
|
||||
|
||||
# ┳┓
|
||||
# ┣┫┏┓┏┏┓┏┓┏┓┏┏┓
|
||||
# ┛┗┗ ┛┣┛┗┛┛┗┛┗
|
||||
# ┛
|
||||
|
||||
# Make the mail client a label:
|
||||
mail_client = {
|
||||
"gmail": "Gmail",
|
||||
"outlook": "Outlook"
|
||||
}.get(mail_client, mail_client)
|
||||
|
||||
# If there was some exception:
|
||||
if exception: return await render_template(
|
||||
"/message/mail/oauth/oauth_failure_v2.html",
|
||||
mail_client = mail_client,
|
||||
failure_hint = (
|
||||
f"An internal server error occurred. "
|
||||
f"Please use log-id '{kwargs.get('log_id')}' to check with the support team."
|
||||
)
|
||||
)
|
||||
|
||||
# For an invalid client:
|
||||
if client_controller is None or client_connector is None:
|
||||
return await render_template(
|
||||
"/message/mail/oauth/oauth_failure_v2.html",
|
||||
mail_client = mail_client,
|
||||
failure_hint = (
|
||||
f"Invalid client '{mail_client}' selected. "
|
||||
f"Please use log-id '{kwargs.get('log_id')}' to check with the support team."
|
||||
)
|
||||
)
|
||||
|
||||
# For a valid client whose authorization was denied/cancelled:
|
||||
if client_response.action in ["denied", "cancelled"]:
|
||||
return await render_template(
|
||||
"/message/mail/oauth/oauth_cancelled_v2.html",
|
||||
mail_client = mail_client
|
||||
)
|
||||
|
||||
# For successful authorization:
|
||||
if client_response.success:
|
||||
return await render_template(
|
||||
"/message/mail/oauth/oauth_success_v2.html",
|
||||
mail_client = mail_client
|
||||
)
|
||||
|
||||
# For failed authorization:
|
||||
return await render_template(
|
||||
"/message/mail/oauth/oauth_failure_v2.html",
|
||||
mail_client = mail_client.title(),
|
||||
failure_hint = f"Unknown error. Please use log-id '{kwargs.get('log_id')}' to check with the support team."
|
||||
)
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MAIN PROGRAM ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,241 @@
|
||||
"""
|
||||
|
||||
AUTHOR:
|
||||
|
||||
Khushal P Soonderji
|
||||
|
||||
DATE:
|
||||
|
||||
Wednesday, 27th Nov., 2024
|
||||
|
||||
OBJECTIVE:
|
||||
|
||||
To receive authorization requests (OAuth2.0) for various mail providers and accordingly respond with the
|
||||
authorization request URLs.
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
# GMail-related utils:
|
||||
from utils_v2.goog.controllers.gmail.gmail_client import SCOPES_GMAIL_MAIL_MANAGEMENT
|
||||
|
||||
# Common:
|
||||
from shared import constants
|
||||
|
||||
# Data Models:
|
||||
from models.api.message.mail.oauth import (
|
||||
OAuthMailAuthorizationRequestHeaders,
|
||||
OAuthMailAuthorizationRequestData
|
||||
)
|
||||
from models.core.auth_token import CoreAuthTokenModel
|
||||
|
||||
# For asynchronous activities:
|
||||
import asyncio
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MACROS / ONE-TIME INIT ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# Related to Quart:
|
||||
mail_oauth_request_bp = Blueprint("mail_oauth", __name__)
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** VARIABLES ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# --- Nothing Yet
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** FUNCTIONS ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
@mail_oauth_request_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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@mail_oauth_request_bp.route("/oauth", 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 = "mailOAuthUrlReqApi",
|
||||
log_input = True,
|
||||
log_output = True,
|
||||
sensitive_keys = ["sessionToken", "X-Session-Token"]
|
||||
)
|
||||
@log_chain_to_mongo(attr_name = "logs_mongo")
|
||||
@should_not_be_under_maintenance(attr_name = "is_under_maintenance")
|
||||
@validate_input(
|
||||
header_validator = lambda x: OAuthMailAuthorizationRequestHeaders(**x).model_dump(),
|
||||
data_validator = lambda x: OAuthMailAuthorizationRequestData(**x)
|
||||
)
|
||||
@handle_cancelled_request()
|
||||
async def request_oauth_authorization_url(
|
||||
inbound_headers: dict | OAuthMailAuthorizationRequestHeaders = None,
|
||||
inbound_data: dict | OAuthMailAuthorizationRequestData = None,
|
||||
inbound_files: dict = None,
|
||||
**kwargs
|
||||
):
|
||||
|
||||
"""
|
||||
Use this when requesting access to someone's GMail account. This API should be used from the UI. A button click
|
||||
"Connect to GMail" should hit this API, which will generate a request to gain access to the user's GMail account.
|
||||
When the URL is hit, it opens Google's own UI, and, when the user clicks "Continue", Google hits your 'redirect_url'
|
||||
to inform you about the user's action.
|
||||
: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
|
||||
)
|
||||
|
||||
# Start by assuming failure:
|
||||
auth_url = None
|
||||
|
||||
# ┏┓ ┏┳┓ ┓ ┓┏┓
|
||||
# ┃┓┏┓┏┓┏┓┏┓┏┓╋┏┓ ┃ ┏┓┃┏┏┓┏┓ ┃┫ ┏┓┓┏
|
||||
# ┗┛┗ ┛┗┗ ┛ ┗┻┗┗ ┻ ┗┛┛┗┗ ┛┗ ┛┗┛┗ ┗┫
|
||||
# ┛
|
||||
|
||||
# Make a user identifier from the session info:
|
||||
token_key = await current_app.mail_controller.generate_token_key(
|
||||
db_conn = current_app.sql_writer,
|
||||
mongo_conn = current_app.data_mongo,
|
||||
auth_token = CoreAuthTokenModel(
|
||||
serviceType = "email",
|
||||
client = inbound_data.mailClient,
|
||||
authType = "oauth",
|
||||
user = kwargs["session_info"],
|
||||
clientUserId = {"email": inbound_data.mailId},
|
||||
status = "pending",
|
||||
syncFreq = inbound_data.syncFreq,
|
||||
),
|
||||
session_token = inbound_headers["X-Session-Token"]
|
||||
)
|
||||
if token_key is None:
|
||||
return ResponseModel(
|
||||
status_code = StatusCodes.FAILED,
|
||||
http_code = HttpCodes.INTERNAL_SERVER_ERROR,
|
||||
message = "Failed to generate token key."
|
||||
)
|
||||
|
||||
# ┏┓ ┏┓┳┳┓ •┓
|
||||
# ┣ ┏┓┏┓ ┃┓┃┃┃┏┓┓┃
|
||||
# ┻ ┗┛┛ ┗┛┛ ┗┗┻┗┗
|
||||
|
||||
if inbound_data.mailClient == "gmail":
|
||||
|
||||
# Get the authorization URL:
|
||||
auth_url = await current_app.gmail_client.get_authorization_url(
|
||||
scopes = SCOPES_GMAIL_MAIL_MANAGEMENT,
|
||||
state = str(token_key),
|
||||
access_type = "offline",
|
||||
approval_prompt = "force",
|
||||
include_granted_scopes = "true",
|
||||
user_email = inbound_data.mailId
|
||||
)
|
||||
|
||||
# ┳┓
|
||||
# ┣┫┏┓┏┏┓┏┓┏┓┏┏┓
|
||||
# ┛┗┗ ┛┣┛┗┛┛┗┛┗
|
||||
# ┛
|
||||
|
||||
# Done here:
|
||||
return ResponseModel(
|
||||
status_code = StatusCodes.OK if auth_url else StatusCodes.FAILED,
|
||||
http_code = HttpCodes.SUCCESS if auth_url else HttpCodes.INTERNAL_SERVER_ERROR,
|
||||
data = {
|
||||
"client": inbound_data.mailClient,
|
||||
"authorizationUrl": auth_url
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MAIN PROGRAM ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,229 @@
|
||||
"""
|
||||
|
||||
AUTHOR:
|
||||
|
||||
Khushal P Soonderji
|
||||
|
||||
DATE:
|
||||
|
||||
Thursday, 16th jan., 2025.
|
||||
|
||||
OBJECTIVE:
|
||||
|
||||
To receive authorization requests (OAuth2.0) for various mail providers and accordingly respond with the
|
||||
authorization request URLs.
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
# GMail-related utils:
|
||||
from utils_v2.goog.controllers.gmail.gmail_client import SCOPES_GMAIL_MAIL_MANAGEMENT
|
||||
|
||||
# Common:
|
||||
from shared import constants
|
||||
|
||||
# Data Models:
|
||||
from models.core.user import CoreUserInfoModel
|
||||
from models.api.message.mail.oauth import (
|
||||
OAuthMailAuthorizationRequestHeaders,
|
||||
OAuthMailAuthorizationRequestData
|
||||
)
|
||||
from models.core.auth_token import CoreAuthTokenModel
|
||||
|
||||
# For asynchronous activities:
|
||||
import asyncio
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MACROS / ONE-TIME INIT ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# Related to Quart:
|
||||
mail_oauth_request_bp = Blueprint("mail_oauth", __name__)
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** VARIABLES ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# --- Nothing Yet
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** FUNCTIONS ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
@mail_oauth_request_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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@mail_oauth_request_bp.route("/oauth", 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 = "mailOAuthUrlReqApi",
|
||||
log_input = True,
|
||||
log_output = True,
|
||||
sensitive_keys = ["sessionToken", "X-Session-Token"]
|
||||
)
|
||||
@log_chain_to_mongo(attr_name = "logs_mongo")
|
||||
@should_not_be_under_maintenance(attr_name = "is_under_maintenance")
|
||||
@validate_input(
|
||||
header_validator = lambda x: OAuthMailAuthorizationRequestHeaders(**x).model_dump(),
|
||||
data_validator = lambda x: OAuthMailAuthorizationRequestData(**x)
|
||||
)
|
||||
@handle_cancelled_request()
|
||||
async def request_oauth_authorization_url(
|
||||
inbound_headers: dict | OAuthMailAuthorizationRequestHeaders = None,
|
||||
inbound_data: dict | OAuthMailAuthorizationRequestData = None,
|
||||
inbound_files: dict = None,
|
||||
**kwargs
|
||||
):
|
||||
|
||||
"""
|
||||
Use this when requesting access to someone's GMail account. This API should be used from the UI. A button click
|
||||
"Connect to GMail" should hit this API, which will generate a request to gain access to the user's GMail account.
|
||||
When the URL is hit, it opens Google's own UI, and, when the user clicks "Continue", Google hits your 'redirect_url'
|
||||
to inform you about the user's action.
|
||||
: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
|
||||
)
|
||||
|
||||
# ┳┓ ┳┳┓ •┓ ┏┓┓•
|
||||
# ┣┫┏┓┓┏╋┏┓ ╋┏┓ ┃┃┃┏┓┓┃ ┃ ┃┓┏┓┏┓╋
|
||||
# ┛┗┗┛┗┻┗┗ ┗┗┛ ┛ ┗┗┻┗┗ ┗┛┗┗┗ ┛┗┗
|
||||
|
||||
# Start by assuming failure:
|
||||
client_controller = None
|
||||
client_connector = None
|
||||
client_response = None
|
||||
|
||||
# Figure out the client connector:
|
||||
match inbound_data.mailClient:
|
||||
case "gmail": client_controller, client_connector = current_app.gmail_controller, current_app.gmail_client
|
||||
case _: client_controller, client_connector = None, None
|
||||
|
||||
# If a client connector was matched:
|
||||
if client_controller is not None and client_connector is not None:
|
||||
client_response = await client_controller.get_authorization_url(
|
||||
sql_conn = current_app.sql_writer,
|
||||
mongo_data_conn = current_app.data_mongo,
|
||||
mail_client = client_connector,
|
||||
user_info = CoreUserInfoModel(**kwargs["session_info"]),
|
||||
inbound_data = inbound_data,
|
||||
session_token = inbound_headers["X-Session-Token"]
|
||||
)
|
||||
|
||||
# ┳┓
|
||||
# ┣┫┏┓┏┏┓┏┓┏┓┏┏┓
|
||||
# ┛┗┗ ┛┣┛┗┛┛┗┛┗
|
||||
# ┛
|
||||
|
||||
# Unknown client:
|
||||
if client_controller is None or client_connector is None: return ResponseModel(
|
||||
status_code = StatusCodes.FAILED,
|
||||
http_code = HttpCodes.BAD_REQUEST,
|
||||
message = f"Unknown/unimplemented client '{inbound_data.mailClient}'."
|
||||
)
|
||||
|
||||
# Known client (could be a success or a failure):
|
||||
return ResponseModel(
|
||||
status_code = StatusCodes.OK if client_response.success else StatusCodes.FAILED,
|
||||
http_code = HttpCodes.SUCCESS if client_response.success else HttpCodes.INTERNAL_SERVER_ERROR,
|
||||
message = client_response.message,
|
||||
data = {
|
||||
"client": inbound_data.mailClient,
|
||||
"authorizationUrl": client_response.url
|
||||
} if client_response.success else None
|
||||
)
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MAIN PROGRAM ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,223 @@
|
||||
"""
|
||||
|
||||
AUTHOR:
|
||||
|
||||
Khushal P Soonderji
|
||||
|
||||
DATE:
|
||||
|
||||
Tuesday, 3rd Dec., 2024
|
||||
|
||||
OBJECTIVE:
|
||||
|
||||
To get one full mail for any given user for any given account.
|
||||
|
||||
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, g, request
|
||||
|
||||
# My utils:
|
||||
from utils_v2.string import json
|
||||
from utils_v2.database.async_mongo_v2 import AsyncMongo
|
||||
from utils_v2.api.codes import StatusCodes, HttpCodes
|
||||
from utils_v2.api.response import ResponseModel
|
||||
from utils_v2.api.async_quart import (
|
||||
make_ordered_json,
|
||||
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
|
||||
)
|
||||
|
||||
# GMail-related utils:
|
||||
from utils_v2.goog.controllers.gmail.gmail_client import SCOPES_GMAIL_MAIL_MANAGEMENT
|
||||
from utils_v2.goog.models.auth_tokens import GoogleAuthTokens
|
||||
|
||||
# Common:
|
||||
from shared import constants
|
||||
|
||||
# Data Models:
|
||||
from models.api.message.mail.get import MailGetRequestHeaders, MailGetRequestData
|
||||
from models.core.user import CoreUserInfoModel
|
||||
|
||||
# To work with datatypes:
|
||||
from typing import Literal
|
||||
|
||||
# For asynchronous activities:
|
||||
import asyncio
|
||||
|
||||
# To work with date and time:
|
||||
import datetime
|
||||
|
||||
# Helpers:
|
||||
from api.helpers.user import token_check
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MACROS / ONE-TIME INIT ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# Related to Quart:
|
||||
mail_get_bp = Blueprint("mail_get", __name__)
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** VARIABLES ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# --- Nothing Yet
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** FUNCTIONS ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
@mail_get_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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@mail_get_bp.route("", 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 = "mailGetApi",
|
||||
log_input = True,
|
||||
log_output = True,
|
||||
sensitive_keys = ["sessionToken", "X-Session-Token"]
|
||||
)
|
||||
@log_chain_to_mongo(attr_name = "logs_mongo")
|
||||
@should_not_be_under_maintenance(attr_name = "is_under_maintenance")
|
||||
@validate_input(
|
||||
header_validator = lambda x: MailGetRequestHeaders(**x).model_dump(),
|
||||
data_validator = lambda x: MailGetRequestData(**x)
|
||||
)
|
||||
@handle_cancelled_request()
|
||||
async def get_one_mail(
|
||||
inbound_headers: dict | MailGetRequestHeaders = None,
|
||||
inbound_data: dict | MailGetRequestData = None,
|
||||
inbound_files: dict = None,
|
||||
**kwargs
|
||||
):
|
||||
|
||||
"""
|
||||
Use this endpoint when the user wants to fetch one mail's full payload.
|
||||
: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,
|
||||
message = "Invalid session."
|
||||
)
|
||||
|
||||
# ┏┓ ┓ ┳┳┓ •┓
|
||||
# ┣ ┏┓╋┏┣┓ ┃┃┃┏┓┓┃
|
||||
# ┻ ┗ ┗┗┛┗ ┛ ┗┗┻┗┗
|
||||
|
||||
# Get the mail:
|
||||
message = await current_app.mail_controller.get_one_mail(
|
||||
mongo_conn = current_app.data_mongo,
|
||||
message_id = inbound_data.messageId
|
||||
)
|
||||
|
||||
# ┏┓ ┓ • ┏┓┓ ┓
|
||||
# ┃┃┓┏┏┏┓┏┓┏┓┏┣┓┓┏┓ ┃ ┣┓┏┓┏┃┏
|
||||
# ┗┛┗┻┛┛┗┗ ┛ ┛┛┗┗┣┛ ┗┛┛┗┗ ┗┛┗
|
||||
# ┛
|
||||
|
||||
# We check if the token that was used to fetch the mail is owned by this user:
|
||||
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 = "The message does not belong to this user."
|
||||
)
|
||||
|
||||
# ┳┓
|
||||
# ┣┫┏┓┏┏┓┏┓┏┓┏┏┓
|
||||
# ┛┗┗ ┛┣┛┗┛┛┗┛┗
|
||||
# ┛
|
||||
|
||||
# Done here:
|
||||
return ResponseModel(
|
||||
status_code = StatusCodes.OK if message else StatusCodes.FAILED,
|
||||
http_code = HttpCodes.SUCCESS if message else HttpCodes.NOT_FOUND,
|
||||
data = message.full
|
||||
)
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MAIN PROGRAM ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,221 @@
|
||||
"""
|
||||
|
||||
AUTHOR:
|
||||
|
||||
Khushal P Soonderji
|
||||
|
||||
DATE:
|
||||
|
||||
Tuesday, 3rd Dec., 2024
|
||||
|
||||
OBJECTIVE:
|
||||
|
||||
To list e-mails by their token identifier. Remember that the 'token identifier' is the '_id' of the document in
|
||||
MongoDB that holds the tokens to authorize the e-mail id whose mails are being accessed.
|
||||
|
||||
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, g, request
|
||||
|
||||
# My utils:
|
||||
from utils_v2.string import json
|
||||
from utils_v2.database.async_mongo_v2 import AsyncMongo
|
||||
from utils_v2.api.codes import StatusCodes, HttpCodes
|
||||
from utils_v2.api.response import ResponseModel
|
||||
from utils_v2.api.async_quart import (
|
||||
make_ordered_json,
|
||||
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
|
||||
)
|
||||
|
||||
# GMail-related utils:
|
||||
from utils_v2.goog.controllers.gmail.gmail_client import SCOPES_GMAIL_MAIL_MANAGEMENT
|
||||
from utils_v2.goog.models.auth_tokens import GoogleAuthTokens
|
||||
|
||||
# Common:
|
||||
from shared import constants
|
||||
|
||||
# Data Models:
|
||||
from models.api.message.mail.list import MailListRequestHeaders, MailListRequestData
|
||||
|
||||
# To work with datatypes:
|
||||
from typing import Literal
|
||||
|
||||
# For asynchronous activities:
|
||||
import asyncio
|
||||
|
||||
# To work with date and time:
|
||||
import datetime
|
||||
|
||||
# Helpers:
|
||||
from api.helpers.user import token_check
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MACROS / ONE-TIME INIT ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# Related to Quart:
|
||||
mail_list_bp = Blueprint("mail_list", __name__)
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** VARIABLES ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# --- Nothing Yet
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** FUNCTIONS ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
@mail_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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@mail_list_bp.route("/list", methods = ["GET"])
|
||||
@mail_list_bp.route("/list/id/token", methods = ["GET", "POST"])
|
||||
@mail_list_bp.route("/list/tags", methods = ["GET", "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 = "mailListApi",
|
||||
log_input = True,
|
||||
log_output = 1,
|
||||
sensitive_keys = ["sessionToken", "X-Session-Token"]
|
||||
)
|
||||
@log_chain_to_mongo(attr_name = "logs_mongo")
|
||||
@should_not_be_under_maintenance(attr_name = "is_under_maintenance")
|
||||
@validate_input(
|
||||
header_validator = lambda x: MailListRequestHeaders(**x).model_dump(),
|
||||
data_validator = lambda x: MailListRequestData(**x)
|
||||
)
|
||||
@handle_cancelled_request()
|
||||
async def list_mails(
|
||||
inbound_headers: dict | MailListRequestHeaders = None,
|
||||
inbound_data: dict | MailListRequestData = None,
|
||||
inbound_files: dict = None,
|
||||
**kwargs
|
||||
):
|
||||
|
||||
"""
|
||||
Use this endpoint when the user wants to fetch the list of mails. The shortlisting here will be done by way of the
|
||||
token identifier. Any no. of 'tokenId' objects will be given by the client.
|
||||
: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,
|
||||
message = "invalid session"
|
||||
)
|
||||
|
||||
# ┏┓ ┓• ┳┳┓ •┓
|
||||
# ┣ ┏┓┃┓┏╋ ┃┃┃┏┓┓┃┏
|
||||
# ┗┛┛┗┗┗┛┗ ┛ ┗┗┻┗┗┛
|
||||
|
||||
# Get the token ids from the token keys:
|
||||
auth_tokens = await current_app.mail_controller.get_tokens_from_keys(
|
||||
mongo_conn = current_app.data_mongo,
|
||||
token_keys = inbound_data.tokenKeys
|
||||
)
|
||||
token_ids = [t.authTokenId for t in auth_tokens]
|
||||
|
||||
# Build the additional filter:
|
||||
additional_filter = {}
|
||||
if inbound_data.tags: additional_filter["tags"] = {"$in": inbound_data.tags}
|
||||
additional_filter = additional_filter or None
|
||||
|
||||
# Get the mails:
|
||||
mails_list = await current_app.mail_controller.list_mails(
|
||||
mongo_conn = current_app.data_mongo,
|
||||
token_ids = token_ids,
|
||||
limit = inbound_data.count,
|
||||
skip = inbound_data.fromCount,
|
||||
additional_filter = additional_filter
|
||||
)
|
||||
|
||||
# Done here:
|
||||
return ResponseModel(
|
||||
status_code = StatusCodes.OK if mails_list else StatusCodes.FAILED,
|
||||
http_code = HttpCodes.SUCCESS if mails_list else HttpCodes.NOT_FOUND,
|
||||
data = [mail.preview for mail in mails_list],
|
||||
message = f"{len(mails_list) if mails_list else 0} mail(s) found"
|
||||
)
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MAIN PROGRAM ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,230 @@
|
||||
"""
|
||||
|
||||
AUTHOR:
|
||||
|
||||
Khushal P Soonderji
|
||||
|
||||
DATE:
|
||||
|
||||
Thursday, 19th Dec., 2024
|
||||
|
||||
OBJECTIVE:
|
||||
|
||||
To send mails.
|
||||
|
||||
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, g, request
|
||||
|
||||
# My utils:
|
||||
from utils_v2.string import json
|
||||
from utils_v2.database.async_mongo_v2 import AsyncMongo
|
||||
from utils_v2.api.codes import StatusCodes, HttpCodes
|
||||
from utils_v2.api.response import ResponseModel
|
||||
from utils_v2.api.async_quart import (
|
||||
make_ordered_json,
|
||||
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
|
||||
)
|
||||
|
||||
# GMail-related utils:
|
||||
from utils_v2.goog.controllers.gmail.gmail_client import SCOPES_GMAIL_MAIL_MANAGEMENT
|
||||
from utils_v2.goog.models.auth_tokens import GoogleAuthTokens
|
||||
|
||||
# Common:
|
||||
from shared import constants
|
||||
|
||||
# Data Models:
|
||||
from models.api.message.mail.send import MailSendRequestHeaders, MailSendRequestData
|
||||
from models.core.user import CoreUserInfoModel
|
||||
|
||||
# To work with datatypes:
|
||||
from typing import Literal
|
||||
|
||||
# For asynchronous activities:
|
||||
import asyncio
|
||||
|
||||
# To work with date and time:
|
||||
import datetime
|
||||
|
||||
# Helpers:
|
||||
from api.helpers.user import token_check
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MACROS / ONE-TIME INIT ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# Related to Quart:
|
||||
mail_send_bp = Blueprint("mail_send", __name__)
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** VARIABLES ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# --- Nothing Yet
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** FUNCTIONS ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
@mail_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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@mail_send_bp.route("", 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 = "mailSendApi",
|
||||
log_input = True,
|
||||
log_output = True,
|
||||
sensitive_keys = ["sessionToken", "X-Session-Token"]
|
||||
)
|
||||
@log_chain_to_mongo(attr_name = "logs_mongo")
|
||||
@should_not_be_under_maintenance(attr_name = "is_under_maintenance")
|
||||
@validate_input(
|
||||
header_validator = lambda x: MailSendRequestHeaders(**x).model_dump(),
|
||||
data_validator = lambda x: MailSendRequestData(**x)
|
||||
)
|
||||
@handle_cancelled_request()
|
||||
async def send_one_mail(
|
||||
inbound_headers: dict | MailSendRequestHeaders = None,
|
||||
inbound_data: dict | MailSendRequestData = None,
|
||||
inbound_files: dict = None,
|
||||
**kwargs
|
||||
):
|
||||
|
||||
"""
|
||||
Use this endpoint to send one mail message.
|
||||
: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.
|
||||
"""
|
||||
|
||||
# Start by assuming failure:
|
||||
success = False
|
||||
|
||||
# ┏┓ ┓ ┏┓┓ ┓
|
||||
# ┣┫┓┏╋┣┓ ┃ ┣┓┏┓┏┃┏
|
||||
# ┛┗┗┻┗┛┗ ┗┛┛┗┗ ┗┛┗
|
||||
|
||||
# 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 token based on the key:
|
||||
auth_token = await current_app.mail_controller.get_token_from_key(
|
||||
mongo_conn = current_app.data_mongo,
|
||||
token_key = inbound_data.tokenKey
|
||||
)
|
||||
|
||||
print("INBOUND DATA:", json.to_string(inbound_data.model_dump(), default=str))
|
||||
print("INBOUND FILES:", json.to_string(inbound_files, default=str))
|
||||
print("AUTH TOKEN:", json.to_string(auth_token, default=str))
|
||||
|
||||
# We check if the token that was used to fetch the mail is owned by this user:
|
||||
if not await token_check.is_authorized(
|
||||
mongo_conn = current_app.data_mongo,
|
||||
user_info = CoreUserInfoModel(**kwargs["session_info"]),
|
||||
token_ids = [auth_token.authTokenId]
|
||||
): return ResponseModel(
|
||||
status_code = StatusCodes.FAILED,
|
||||
http_code = HttpCodes.UNAUTHORIZED,
|
||||
message = "The account does not belong to this user."
|
||||
)
|
||||
|
||||
# ┏┓ ┓ ┳┳┓ •┓
|
||||
# ┗┓┏┓┏┓┏┫ ┃┃┃┏┓┓┃
|
||||
# ┗┛┗ ┛┗┗┻ ┛ ┗┗┻┗┗
|
||||
|
||||
pass
|
||||
|
||||
# ┳┓
|
||||
# ┣┫┏┓┏┏┓┏┓┏┓┏┏┓
|
||||
# ┛┗┗ ┛┣┛┗┛┛┗┛┗
|
||||
# ┛
|
||||
|
||||
# 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
|
||||
@@ -0,0 +1,256 @@
|
||||
"""
|
||||
|
||||
AUTHOR:
|
||||
|
||||
Khushal P Soonderji
|
||||
|
||||
DATE:
|
||||
|
||||
Monday, 2nd Dec., 2024
|
||||
|
||||
OBJECTIVE:
|
||||
|
||||
To receive requests for synchronising mails from various mail clients to the database. Sync'ing means we pull
|
||||
the mail from the mail client (like GMail) and store it to our database. The mail is then ready for showing on
|
||||
the UI at any time.
|
||||
|
||||
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, g, request
|
||||
|
||||
# My utils:
|
||||
from utils_v2.string import json
|
||||
from utils_v2.database.async_mongo_v2 import AsyncMongo
|
||||
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
|
||||
)
|
||||
|
||||
# GMail-related utils:
|
||||
from utils_v2.goog.controllers.gmail.gmail_client import SCOPES_GMAIL_MAIL_MANAGEMENT
|
||||
from utils_v2.goog.models.auth_tokens import GoogleAuthTokens
|
||||
|
||||
# Common:
|
||||
from shared import constants
|
||||
|
||||
# Data Models:
|
||||
from models.core.user import CoreUserInfoModel
|
||||
from models.api.message.mail.sync import MailSyncRequestHeaders, MailSyncRequestData
|
||||
from models.message.mail.sync import MailSyncOneResult, MailSyncManyResults
|
||||
|
||||
# To work with datatypes:
|
||||
from typing import Literal
|
||||
|
||||
# For asynchronous activities:
|
||||
import asyncio
|
||||
|
||||
# To work with LLMs:
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
# To work with date and time:
|
||||
import datetime
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MACROS / ONE-TIME INIT ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# Related to Quart:
|
||||
mail_sync_bp = Blueprint("mail_sync", __name__)
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** VARIABLES ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# --- Nothing Yet
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** FUNCTIONS ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
@mail_sync_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 sync_mails(
|
||||
user_info: CoreUserInfoModel,
|
||||
inbound_headers: dict,
|
||||
inbound_data: MailSyncRequestData
|
||||
) -> MailSyncManyResults:
|
||||
|
||||
"""
|
||||
A very simple function, but kept separate so that we get the option to switch between running it in the foreground
|
||||
and running it in the background.
|
||||
:param user_info: The information of the user as extracted from the session token.
|
||||
:param inbound_headers: The headers that came in with the request.
|
||||
:param inbound_data: The data that came in with the request.
|
||||
:return: The results of the mail-sync'ing attempt.
|
||||
"""
|
||||
|
||||
# Try to sync the mails:
|
||||
return await current_app.mail_controller.sync(
|
||||
db_conn = current_app.sql_writer,
|
||||
mongo_conn = current_app.data_mongo,
|
||||
user_info = user_info,
|
||||
token_key = inbound_data.tokenKey,
|
||||
llm = current_app.llm,
|
||||
force_sync = inbound_data.forceSync,
|
||||
start_date = inbound_data.startDate,
|
||||
end_date = inbound_data.endDate,
|
||||
max_count = inbound_data.maxCount,
|
||||
session_token = inbound_headers["X-Session-Token"],
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@mail_sync_bp.route("/sync", methods = ["POST"])
|
||||
@mail_sync_bp.route("/sync/<mode>", 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 = "mailSyncApi",
|
||||
log_input = True,
|
||||
log_output = True,
|
||||
sensitive_keys = ["sessionToken", "X-Session-Token"]
|
||||
)
|
||||
@log_chain_to_mongo(attr_name = "logs_mongo")
|
||||
@should_not_be_under_maintenance(attr_name = "is_under_maintenance")
|
||||
@validate_input(
|
||||
header_validator = lambda x: MailSyncRequestHeaders(**x).model_dump(),
|
||||
data_validator = lambda x: MailSyncRequestData(**x)
|
||||
)
|
||||
@handle_cancelled_request()
|
||||
async def sync_mail(
|
||||
mode: Literal["background", "bg"] = None,
|
||||
inbound_headers: dict | MailSyncRequestHeaders = None,
|
||||
inbound_data: dict | MailSyncRequestData = None,
|
||||
inbound_files: dict = None,
|
||||
**kwargs
|
||||
):
|
||||
|
||||
"""
|
||||
Use this when the user wants to pull old mails from some mail client (like GMail) and save it to the database for
|
||||
ready access on the UI.
|
||||
:param mode: Set it to one of the specified options to make the sync'ing process go to the background.
|
||||
: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
|
||||
)
|
||||
|
||||
# Make the variables available in the scope of the current request:
|
||||
g.inbound_headers = inbound_headers
|
||||
g.inbound_data = inbound_data
|
||||
|
||||
# If we've been asked to sync the mails in the background:
|
||||
if mode in ["background", "bg"]:
|
||||
current_app.add_background_task(
|
||||
sync_mails,
|
||||
user_info = CoreUserInfoModel(**kwargs["session_info"]),
|
||||
inbound_headers = inbound_headers,
|
||||
inbound_data = inbound_data
|
||||
)
|
||||
return ResponseModel(
|
||||
status_code = StatusCodes.OK,
|
||||
http_code = HttpCodes.ACCEPTED,
|
||||
message = "your mails are being sync'd in the background"
|
||||
)
|
||||
|
||||
# Otherwise we process it right here:
|
||||
sync_results = await sync_mails(
|
||||
user_info = CoreUserInfoModel(**kwargs["session_info"]),
|
||||
inbound_headers = inbound_headers,
|
||||
inbound_data = inbound_data
|
||||
)
|
||||
|
||||
# Response:
|
||||
return ResponseModel(
|
||||
status_code = StatusCodes.FAILED if sync_results.failureCount > 0 else StatusCodes.OK,
|
||||
http_code = HttpCodes.INTERNAL_SERVER_ERROR if sync_results.failureCount > 0 else HttpCodes.SUCCESS,
|
||||
message = sync_results.message,
|
||||
data = {
|
||||
"totalCount": sync_results.totalCount,
|
||||
"successCount": sync_results.successCount,
|
||||
"failureCount": sync_results.failureCount
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MAIN PROGRAM ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,234 @@
|
||||
"""
|
||||
|
||||
AUTHOR:
|
||||
|
||||
Khushal P Soonderji
|
||||
|
||||
DATE:
|
||||
|
||||
Friday, 13th Dec., 2024
|
||||
|
||||
OBJECTIVE:
|
||||
|
||||
To update the tags on one mail message.
|
||||
|
||||
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, g, request
|
||||
|
||||
# My utils:
|
||||
from utils_v2.string import json
|
||||
from utils_v2.database.async_mongo_v2 import AsyncMongo
|
||||
from utils_v2.api.codes import StatusCodes, HttpCodes
|
||||
from utils_v2.api.response import ResponseModel
|
||||
from utils_v2.api.async_quart import (
|
||||
make_ordered_json,
|
||||
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
|
||||
)
|
||||
|
||||
# GMail-related utils:
|
||||
from utils_v2.goog.controllers.gmail.gmail_client import SCOPES_GMAIL_MAIL_MANAGEMENT
|
||||
from utils_v2.goog.models.auth_tokens import GoogleAuthTokens
|
||||
|
||||
# Common:
|
||||
from shared import constants
|
||||
|
||||
# Data Models:
|
||||
from models.api.message.mail.tags import MailUpdateTagsRequestHeaders, MailUpdateTagsRequestData
|
||||
from models.core.user import CoreUserInfoModel
|
||||
|
||||
# To work with datatypes:
|
||||
from typing import Literal
|
||||
|
||||
# For asynchronous activities:
|
||||
import asyncio
|
||||
|
||||
# To work with date and time:
|
||||
import datetime
|
||||
|
||||
# Helpers:
|
||||
from api.helpers.user import token_check
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MACROS / ONE-TIME INIT ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# Related to Quart:
|
||||
mail_tags_update_bp = Blueprint("mail_tags_update", __name__)
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** VARIABLES ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# --- Nothing Yet
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** FUNCTIONS ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
@mail_tags_update_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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@mail_tags_update_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 = "mailTagsUpdtApi",
|
||||
log_input = True,
|
||||
log_output = True,
|
||||
sensitive_keys = ["sessionToken", "X-Session-Token"]
|
||||
)
|
||||
@log_chain_to_mongo(attr_name = "logs_mongo")
|
||||
@should_not_be_under_maintenance(attr_name = "is_under_maintenance")
|
||||
@validate_input(
|
||||
header_validator = lambda x: MailUpdateTagsRequestHeaders(**x).model_dump(),
|
||||
data_validator = lambda x: MailUpdateTagsRequestData(**x)
|
||||
)
|
||||
@handle_cancelled_request()
|
||||
async def update_mail_tags(
|
||||
inbound_headers: dict | MailUpdateTagsRequestHeaders = None,
|
||||
inbound_data: dict | MailUpdateTagsRequestData = None,
|
||||
inbound_files: dict = None,
|
||||
**kwargs
|
||||
):
|
||||
|
||||
"""
|
||||
Use this endpoint to update the tags on an e-mail message.
|
||||
: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 mail:
|
||||
message = await current_app.mail_controller.get_one_mail(
|
||||
mongo_conn = current_app.data_mongo,
|
||||
message_id = inbound_data.messageId
|
||||
)
|
||||
|
||||
# ┏┓ ┓ • ┏┓┓ ┓
|
||||
# ┃┃┓┏┏┏┓┏┓┏┓┏┣┓┓┏┓ ┃ ┣┓┏┓┏┃┏
|
||||
# ┗┛┗┻┛┛┗┗ ┛ ┛┛┗┗┣┛ ┗┛┛┗┗ ┗┛┗
|
||||
# ┛
|
||||
|
||||
# We check if the token that was used to fetch the mail is owned by this user:
|
||||
if not await token_check.is_authorized(
|
||||
mongo_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 = "The message does not belong to this user."
|
||||
)
|
||||
|
||||
# ┳┳ ┓ ┳┳┓ •┓
|
||||
# ┃┃┏┓┏┫┏┓╋┏┓ ┃┃┃┏┓┓┃
|
||||
# ┗┛┣┛┗┻┗┻┗┗ ┛ ┗┗┻┗┗
|
||||
# ┛
|
||||
|
||||
# Update the mail:
|
||||
success = await current_app.mail_controller.update_tags(
|
||||
mongo_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
|
||||
@@ -0,0 +1,241 @@
|
||||
"""
|
||||
|
||||
AUTHOR:
|
||||
|
||||
Khushal P Soonderji
|
||||
|
||||
DATE:
|
||||
|
||||
Thursday, 19th Dec., 2024
|
||||
|
||||
OBJECTIVE:
|
||||
|
||||
To receive auth details for various SMS client APIs.
|
||||
|
||||
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.api.message.sms.auth import SMSAuthRequestHeaders, SMSAuthRequestData
|
||||
from models.core.auth_token import CoreAuthTokenModel
|
||||
|
||||
# For asynchronous activities:
|
||||
import asyncio
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MACROS / ONE-TIME INIT ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# Related to Quart:
|
||||
sms_auth_bp = Blueprint("sms_auth", __name__)
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** VARIABLES ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# --- Nothing Yet
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** FUNCTIONS ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
@sms_auth_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_auth_bp.route("/auth", 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 = "smsAuthApi",
|
||||
log_input = True,
|
||||
log_output = True,
|
||||
sensitive_keys = ["sessionToken", "X-Session-Token"]
|
||||
)
|
||||
@log_chain_to_mongo(attr_name = "logs_mongo")
|
||||
@should_not_be_under_maintenance(attr_name = "is_under_maintenance")
|
||||
@validate_input(
|
||||
header_validator = lambda x: SMSAuthRequestHeaders(**x).model_dump(),
|
||||
data_validator = lambda x: SMSAuthRequestData(**x)
|
||||
)
|
||||
@handle_cancelled_request()
|
||||
async def authorize_sms_client(
|
||||
inbound_headers: dict | SMSAuthRequestHeaders = None,
|
||||
inbound_data: dict | SMSAuthRequestData = None,
|
||||
inbound_files: dict = None,
|
||||
**kwargs
|
||||
):
|
||||
|
||||
"""
|
||||
Use this when a user wants to register a third-party SMS client with your service.
|
||||
: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
|
||||
)
|
||||
|
||||
# Start by assuming failure:
|
||||
success = False
|
||||
|
||||
# ┏┓ ┳┓• ┓ ┏┓┳┳┓┏┓ ┳ ┓•
|
||||
# ┣ ┏┓┏┓ ┃┃┓┏┳┓┣┓┓┏┏ ┗┓┃┃┃┗┓ ┃┏┓┏┫┓┏┓
|
||||
# ┻ ┗┛┛ ┛┗┗┛┗┗┗┛┗┻┛ ┗┛┛ ┗┗┛ ┻┛┗┗┻┗┗┻
|
||||
|
||||
if inbound_data.smsClient == "nimbusSmsIndia":
|
||||
|
||||
success = await current_app.sms_controller.set_token_direct(
|
||||
sql_conn = current_app.sql_writer,
|
||||
mongo_data_conn = current_app.data_mongo,
|
||||
auth_token = CoreAuthTokenModel(
|
||||
serviceType = "sms",
|
||||
client = inbound_data.smsClient,
|
||||
authType = "auth",
|
||||
auth = inbound_data.auth.model_dump(),
|
||||
user = kwargs.get("session_info"),
|
||||
clientUserId = {
|
||||
"userId": inbound_data.auth.userId,
|
||||
"senderId": inbound_data.auth.senderId,
|
||||
"entityId": inbound_data.auth.entityId
|
||||
},
|
||||
status = "active",
|
||||
syncFreq = 60
|
||||
),
|
||||
token_notes = {},
|
||||
session_token = inbound_headers["X-Session-Token"]
|
||||
)
|
||||
|
||||
# ┏┓ ┏┓ ┳┓ ┓┓ ┏┓┳┳┓┏┓ ┓┏┓
|
||||
# ┣ ┏┓┏┓ ┗┓┏┓┓┏┓┏┓┏ ┣┫┓┏┃┃┏ ┗┓┃┃┃┗┓ ┃┫ ┏┓┏┓┓┏┏┓
|
||||
# ┻ ┗┛┛ ┗┛┗┻┗┛┗┛┗┫ ┻┛┗┻┗┛┗ ┗┛┛ ┗┗┛ ┛┗┛┗ ┛┗┗┫┗┻
|
||||
# ┛ ┛
|
||||
|
||||
elif inbound_data.smsClient == "savvyBulkSmsKenya":
|
||||
|
||||
success = await current_app.sms_controller.set_token_direct(
|
||||
sql_conn = current_app.sql_writer,
|
||||
mongo_data_conn = current_app.data_mongo,
|
||||
auth_token = CoreAuthTokenModel(
|
||||
serviceType = "sms",
|
||||
client = inbound_data.smsClient,
|
||||
authType = "auth",
|
||||
auth = inbound_data.auth.model_dump(),
|
||||
user = kwargs.get("session_info"),
|
||||
clientUserId = {
|
||||
"partnerId": inbound_data.auth.partnerId,
|
||||
"shortCode": inbound_data.auth.shortCode
|
||||
},
|
||||
status = "active",
|
||||
syncFreq = 60
|
||||
),
|
||||
token_notes = {},
|
||||
session_token = inbound_headers["X-Session-Token"]
|
||||
)
|
||||
|
||||
# ┳┓
|
||||
# ┣┫┏┓┏┏┓┏┓┏┓┏┏┓
|
||||
# ┛┗┗ ┛┣┛┗┛┛┗┛┗
|
||||
# ┛
|
||||
|
||||
# Done here:
|
||||
return ResponseModel(
|
||||
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": success
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MAIN PROGRAM ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
pass
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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_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_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
|
||||
Reference in New Issue
Block a user