(20241213) Mail Module Reworked!

This commit is contained in:
2024-12-13 14:14:15 +05:30
parent a702e5ca20
commit e8ddffbdfe
12 changed files with 297 additions and 175 deletions
+1 -1
View File
@@ -110,7 +110,7 @@ def init(blueprint_setup_state):
# --------------------------------------------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------------------------------------------
@llm_invoke_bp.route("/llm/invoke", methods = ["POST"]) @llm_invoke_bp.route("/llm/invoke", methods = ["GET"])
@set_api_version(api_version = "1.0.0") @set_api_version(api_version = "1.0.0")
@read_input(sanitize_headers = False, sanitize_data = False) @read_input(sanitize_headers = False, sanitize_data = False)
@get_session_info(key = "X-Session-Token", session_coro = "get_session") @get_session_info(key = "X-Session-Token", session_coro = "get_session")
+3 -3
View File
@@ -81,7 +81,7 @@ import asyncio
# Related to Quart: # Related to Quart:
mail_callback_bp = Blueprint("mail_cb", __name__) mail_oauth_callback_bp = Blueprint("mail_cb", __name__)
# ***************************************************************************************************************** # *****************************************************************************************************************
@@ -101,7 +101,7 @@ mail_callback_bp = Blueprint("mail_cb", __name__)
# ***************************************************************************************************************** # *****************************************************************************************************************
@mail_callback_bp.record_once @mail_oauth_callback_bp.record_once
def init(blueprint_setup_state): def init(blueprint_setup_state):
# This gets called when the blueprint is registered. # This gets called when the blueprint is registered.
@@ -239,7 +239,7 @@ async def handle_gmail_callback() -> render_template:
# --------------------------------------------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------------------------------------------
@mail_callback_bp.route("/callback/<mail_client>", methods = ["POST", "GET"]) @mail_oauth_callback_bp.route("/callback/<mail_client>", methods = ["POST", "GET"])
@set_api_version(api_version = "1.0.0") @set_api_version(api_version = "1.0.0")
@read_input(sanitize_headers = False, sanitize_data = False) @read_input(sanitize_headers = False, sanitize_data = False)
@log_request_to_mongo( @log_request_to_mongo(
+3 -3
View File
@@ -85,7 +85,7 @@ import asyncio
# Related to Quart: # Related to Quart:
mail_oauth_bp = Blueprint("mail_oauth", __name__) mail_oauth_request_bp = Blueprint("mail_oauth", __name__)
# ***************************************************************************************************************** # *****************************************************************************************************************
@@ -105,7 +105,7 @@ mail_oauth_bp = Blueprint("mail_oauth", __name__)
# ***************************************************************************************************************** # *****************************************************************************************************************
@mail_oauth_bp.record_once @mail_oauth_request_bp.record_once
def init(blueprint_setup_state): def init(blueprint_setup_state):
# This gets called when the blueprint is registered. # This gets called when the blueprint is registered.
@@ -116,7 +116,7 @@ def init(blueprint_setup_state):
# --------------------------------------------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------------------------------------------
@mail_oauth_bp.route("/oauth", methods = ["GET"]) @mail_oauth_request_bp.route("/oauth", methods = ["GET"])
@set_api_version(api_version = "1.0.0") @set_api_version(api_version = "1.0.0")
@read_input(sanitize_headers = False, sanitize_data = False) @read_input(sanitize_headers = False, sanitize_data = False)
@get_session_info(key = "X-Session-Token", session_coro = "get_session") @get_session_info(key = "X-Session-Token", session_coro = "get_session")
+11 -10
View File
@@ -36,8 +36,8 @@
# To make sibling directories accessible for imports: # To make sibling directories accessible for imports:
import sys import sys
sys.path.append(".")
sys.path.append("..") sys.path.append("..")
sys.path.append("../..")
# For using Quart: # For using Quart:
from quart import Blueprint, current_app, g, request from quart import Blueprint, current_app, g, request
@@ -136,13 +136,13 @@ def init(blueprint_setup_state):
@log_chain_to_mongo(attr_name = "logs_mongo") @log_chain_to_mongo(attr_name = "logs_mongo")
@should_not_be_under_maintenance(attr_name = "is_under_maintenance") @should_not_be_under_maintenance(attr_name = "is_under_maintenance")
@validate_input( @validate_input(
header_validator = lambda x: MailGetRequestHeaders(**x).model_dump(), header_validator = lambda x: MailUpdateTagsRequestHeaders(**x).model_dump(),
data_validator = lambda x: MailGetRequestData(**x) data_validator = lambda x: MailUpdateTagsRequestData(**x)
) )
@handle_cancelled_request() @handle_cancelled_request()
async def update_mail_tags( async def update_mail_tags(
inbound_headers: dict | MailGetRequestHeaders = None, inbound_headers: dict | MailUpdateTagsRequestHeaders = None,
inbound_data: dict | MailGetRequestData = None, inbound_data: dict | MailUpdateTagsRequestData = None,
inbound_files: dict = None, inbound_files: dict = None,
**kwargs **kwargs
): ):
@@ -164,17 +164,18 @@ async def update_mail_tags(
) )
# Get the mail: # Get the mail:
message = await current_app.mail_controller.get_one_mail( success = await current_app.mail_controller.update_tags(
mongo_conn = current_app.data_mongo, mongo_conn = current_app.data_mongo,
token_id = inbound_data.tokenId, token_id = inbound_data.tokenId,
message_id = inbound_data.messageId message_id = inbound_data.messageId,
unset_tags = inbound_data.unsetTags,
set_tags = inbound_data.setTags
) )
# Done here: # Done here:
return ResponseModel( return ResponseModel(
status_code = StatusCodes.OK if message else StatusCodes.FAILED, status_code = StatusCodes.OK if success else StatusCodes.FAILED,
http_code = HttpCodes.SUCCESS if message else HttpCodes.NOT_FOUND, http_code = HttpCodes.SUCCESS if success else HttpCodes.INTERNAL_SERVER_ERROR
data = message.full
) )
+2 -16
View File
@@ -108,7 +108,7 @@ def init(blueprint_setup_state):
# --------------------------------------------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------------------------------------------
@test_callback_bp.route("/callback", methods = ["POST", "GET"]) @test_callback_bp.route("/callback", methods = ["GET", "POST", "PUT", "PATCH", "DELETE"])
@set_api_version(api_version = "1.0.0") @set_api_version(api_version = "1.0.0")
@read_input(sanitize_headers = False, sanitize_data = False) @read_input(sanitize_headers = False, sanitize_data = False)
@get_session_info(key = "X-Session-Token", session_coro = "get_session") @get_session_info(key = "X-Session-Token", session_coro = "get_session")
@@ -131,21 +131,7 @@ async def callback_test(
**kwargs **kwargs
): ):
print("SESSION INFO:", kwargs.get("session_info")) return f"ok ({request.method})"
# Return a random page:
return await render_template(
random.choice([
r"/mail/oauth/oauth_success.html",
r"/mail/oauth/oauth_failure.html"
]),
mail_client = random.choice([
"GMail",
"Outlook",
"WhatsApp",
"Telegram"
])
)
# ***************************************************************************************************************** # *****************************************************************************************************************
+16 -22
View File
@@ -42,29 +42,21 @@ import os
import psutil import psutil
# For using Quart: # For using Quart:
from quart import Quart, request, current_app from quart import Quart, current_app
from quart_cors import cors from quart_cors import cors
# Common: # Common:
from shared import constants from shared import constants
# My utils: # My utils:
from utils_v2.string import json
from utils_v2.api import async_quart
from utils_v2.date_time import date_time from utils_v2.date_time import date_time
from utils_v2.database.async_mongo_v2 import AsyncMongo, AsyncMongoStorage from utils_v2.database.async_mongo_v2 import AsyncMongo
from utils_v2.database.async_mysql_v2 import AsyncMySQL from utils_v2.database.async_mysql_v2 import AsyncMySQL
from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache
from utils_v2.serialization.json_serializer import JSONSerializer from utils_v2.serialization.json_serializer import JSONSerializer
from utils_v2.api.async_quart import ( from utils_v2.api.async_quart import (
set_api_version, set_api_version,
read_input, log_request_to_mongo
log_request_to_mongo,
should_not_be_under_maintenance,
only_whitelisted_ips,
limit_rate,
validate_input,
handle_cancelled_request
) )
# GMail-related utils: # GMail-related utils:
@@ -92,11 +84,12 @@ import httpx
from icecream import IceCreamDebugger from icecream import IceCreamDebugger
# All the blueprints: # All the blueprints:
from api.blueprints.mail.oauth_request import mail_oauth_bp from api.blueprints.mail.oauth.request import mail_oauth_request_bp
from api.blueprints.mail.oauth_callback import mail_callback_bp from api.blueprints.mail.oauth.callback import mail_oauth_callback_bp
from api.blueprints.mail.sync_v2 import mail_sync_bp from api.blueprints.mail.sync.sync_v2 import mail_sync_bp
# from api.blueprints.mail.list import mail_list_bp from api.blueprints.mail.retrieve.list import mail_list_bp
# from api.blueprints.mail.retrieve import mail_retrieve_bp from api.blueprints.mail.retrieve.get import mail_get_bp
from api.blueprints.mail.tags.update import mail_tags_update_bp
# from api.blueprints.sms.auth import sms_auth_bp # from api.blueprints.sms.auth import sms_auth_bp
# from api.blueprints.sms.send import sms_send_bp # from api.blueprints.sms.send import sms_send_bp
# from api.blueprints.chat.auth import chat_auth_bp # from api.blueprints.chat.auth import chat_auth_bp
@@ -131,11 +124,12 @@ APP_VERSION = constants.APP_VERSION
# The Quart app: # The Quart app:
app = Quart(__name__, template_folder = r"../views") app = Quart(__name__, template_folder = r"../views")
app = cors(app) app = cors(app)
app.register_blueprint(mail_oauth_bp, url_prefix = f"/{MODULE_BASE}/mail") app.register_blueprint(mail_oauth_request_bp, url_prefix = f"/{MODULE_BASE}/mail")
app.register_blueprint(mail_callback_bp, url_prefix = f"/{MODULE_BASE}/mail") app.register_blueprint(mail_oauth_callback_bp, url_prefix = f"/{MODULE_BASE}/mail")
app.register_blueprint(mail_sync_bp, url_prefix = f"/{MODULE_BASE}/mail") app.register_blueprint(mail_sync_bp, url_prefix = f"/{MODULE_BASE}/mail")
# app.register_blueprint(mail_list_bp, url_prefix = f"/{MODULE_BASE}/mail") app.register_blueprint(mail_list_bp, url_prefix = f"/{MODULE_BASE}/mail")
# app.register_blueprint(mail_retrieve_bp, url_prefix = f"/{MODULE_BASE}/mail") app.register_blueprint(mail_get_bp, url_prefix = f"/{MODULE_BASE}/mail")
app.register_blueprint(mail_tags_update_bp, url_prefix = f"/{MODULE_BASE}/mail")
# app.register_blueprint(sms_auth_bp, url_prefix = f"/{MODULE_BASE}/sms") # app.register_blueprint(sms_auth_bp, url_prefix = f"/{MODULE_BASE}/sms")
# app.register_blueprint(sms_send_bp, url_prefix = f"/{MODULE_BASE}/sms") # app.register_blueprint(sms_send_bp, url_prefix = f"/{MODULE_BASE}/sms")
# app.register_blueprint(chat_auth_bp, url_prefix = f"/{MODULE_BASE}/chat") # app.register_blueprint(chat_auth_bp, url_prefix = f"/{MODULE_BASE}/chat")
@@ -329,7 +323,7 @@ async def app_startup(**kwargs):
# ┃ ┏┓┏┓┏┓ ┃┃┃┏┓┏┫┏┓┃┏ # ┃ ┏┓┏┓┏┓ ┃┃┃┏┓┏┫┏┓┃┏
# ┗┛┗┛┛ ┗ ┛ ┗┗┛┗┻┗ ┗┛ # ┗┛┗┛┛ ┗ ┛ ┗┗┛┗┻┗ ┗┛
current_app.core_message_controller = MessageController( current_app.core_auth_token_controller = AuthTokenController(
cache = current_app.module_cache, cache = current_app.module_cache,
alert_url = current_app.script_data["alerts"]["url"], alert_url = current_app.script_data["alerts"]["url"],
http_client = current_app.http_client, http_client = current_app.http_client,
@@ -337,7 +331,7 @@ async def app_startup(**kwargs):
debug_prefix = "Message (CM) | ", debug_prefix = "Message (CM) | ",
debug_only_errors = True debug_only_errors = True
) )
current_app.core_auth_token_controller = AuthTokenController( current_app.core_message_controller = MessageController(
cache = current_app.module_cache, cache = current_app.module_cache,
alert_url = current_app.script_data["alerts"]["url"], alert_url = current_app.script_data["alerts"]["url"],
http_client = current_app.http_client, http_client = current_app.http_client,
+78 -5
View File
@@ -283,14 +283,12 @@ class MailController:
async def get_token( async def get_token(
mongo_conn: AsyncMongo, mongo_conn: AsyncMongo,
token_id: ObjectId | str = None, token_id: ObjectId | str = None,
**kwargs
) -> CoreAuthTokenModel | None: ) -> CoreAuthTokenModel | None:
# Simply call the core model: # Simply call the core model:
return await current_app.core_auth_token_controller.get_token( return await current_app.core_auth_token_controller.get_token(
mongo_conn = mongo_conn, mongo_conn = mongo_conn,
token_id = token_id, token_id = token_id
**kwargs
) )
# ┏┓ ┳┳┓ # ┏┓ ┳┳┓
@@ -364,8 +362,15 @@ class MailController:
client = auth_token.client, client = auth_token.client,
clientMessageId = message_id, clientMessageId = message_id,
clientThreadId = client_response.data["threadId"], clientThreadId = client_response.data["threadId"],
preview = client_response.data["subject"], isSent = False,
message = client_response.data isBroadcast = False,
sentSuccessfully = False,
sender = client_response.data["from"][0]["name"],
chat = None,
message = client_response.data,
snippet = client_response.data["subject"],
aiSnippet = None,
tags = ["email", "gmail"]
) )
# Give a quick indicator of whether this mail is an inbox mail or sent mail: # Give a quick indicator of whether this mail is an inbox mail or sent mail:
@@ -564,6 +569,74 @@ class MailController:
sync_results.message = f"no such mail client '{auth_token.client}'" sync_results.message = f"no such mail client '{auth_token.client}'"
return sync_results return sync_results
# ┓ • ┏┓ ┏┓ ┳┳┓
# ┃ ┓┏╋ ┣╋ ┃┓┏┓╋ ┃┃┃┏┓┏┏┏┓┏┓┏┓┏
# ┗┛┗┛┗ ┗┻ ┗┛┗ ┗ ┛ ┗┗ ┛┛┗┻┗┫┗ ┛
# ┛
# These are simply for retrieving mails. You need to already have them sync'd to the database. These methods don't
# fetch the mails from the third-party clients.
@staticmethod
async def list_mails(
mongo_conn: AsyncMongo,
token_ids: List[ObjectId | str],
limit: int = 100,
skip: int = 0,
additional_filter: dict = None
) -> List[CoreMessageModel] | None:
# regardless of what additional filter is provided from outside,
# we add a mail-selecting filter here:
if additional_filter is None: additional_filter = {}
additional_filter["serviceType"] = "email"
# Simply call the core model:
return await current_app.core_message_controller.get_previews(
mongo_conn = mongo_conn,
token_ids = token_ids,
limit = limit,
skip = skip,
additional_filter = additional_filter
)
@staticmethod
async def get_one_mail(
mongo_conn: AsyncMongo,
token_id: ObjectId | str,
message_id: ObjectId | str
) -> CoreMessageModel | None:
# Simply call the core model:
return await current_app.core_message_controller.get_message(
mongo_conn = mongo_conn,
token_id = token_id,
message_id = message_id
)
# ┳┳ ┓
# ┃┃┏┓┏┫┏┓╋┏┓
# ┗┛┣┛┗┻┗┻┗┗
# ┛
@staticmethod
async def update_tags(
mongo_conn: AsyncMongo,
token_id: ObjectId | str,
message_id: ObjectId | str,
unset_tags: List[str] = None,
set_tags: List[str] = None
) -> bool:
# Simply call the core model:
return await current_app.core_message_controller.update_tags(
mongo_conn = mongo_conn,
token_id = token_id,
message_id = message_id,
unset_tags = unset_tags,
set_tags = set_tags
)
# ***************************************************************************************************************** # *****************************************************************************************************************
# ***** **** # ***** ****
+1 -11
View File
@@ -272,30 +272,20 @@ class AuthTokenController(BaseModel):
self, self,
mongo_conn: AsyncMongo, mongo_conn: AsyncMongo,
token_id: ObjectId | str = None, token_id: ObjectId | str = None,
**kwargs
) -> CoreAuthTokenModel | None: ) -> CoreAuthTokenModel | None:
""" """
To retrieve stored tokens from the database. To retrieve stored tokens from the database.
:param mongo_conn: The database connection (MongoDB) to use to perform the action. :param mongo_conn: The database connection (MongoDB) to use to perform the action.
:param token_id: The identifier granted by the 'get_token_id' method. :param token_id: The identifier granted by the 'get_token_id' method.
:param kwargs: Any set of key-value pairs to build custom search criteria. This could be things like the user
info, the client, the type of authentication used, or even the kind of service.
:return: The retrieved record that has the token, and information about the service and client if found, else :return: The retrieved record that has the token, and information about the service and client if found, else
None when there is no matching record. None when there is no matching record.
""" """
# Build the filter:
filter_json = {k: v for k, v in kwargs.items()}
if token_id: filter_json["_id"] = ObjectId(token_id)
# If there is no search criteria, we exit with failure:
if not filter_json: return None
# If there is some filtering possible, we fetch the token: # If there is some filtering possible, we fetch the token:
token = await mongo_conn.find_one( token = await mongo_conn.find_one(
collection = self.AUTH_COLLECTION, collection = self.AUTH_COLLECTION,
filter = filter_json, filter = {"_id": ObjectId(token_id)},
) )
# Done here: # Done here:
+84 -64
View File
@@ -129,6 +129,13 @@ class MessageController(BaseModel):
message: CoreMessageModel message: CoreMessageModel
) -> ObjectId: ) -> ObjectId:
"""
Simply insert one message document into the database.
:param mongo_conn: The instance of the database connector to use for the operation.
:param message: The message to save into the database.
:return: The object id of the inserted document.
"""
# Simply insert the document: # Simply insert the document:
return await mongo_conn.insert_one( return await mongo_conn.insert_one(
collection = self.MESSAGES_COLLECTION, collection = self.MESSAGES_COLLECTION,
@@ -139,9 +146,17 @@ class MessageController(BaseModel):
async def bulk_write( async def bulk_write(
self, self,
mongo_conn: AsyncMongo, mongo_conn: AsyncMongo,
mongo_operations mongo_operations: list
) -> int: ) -> int:
"""
Needed in cases like forcing re-sync of mails where you need to perform actions like bulk replacements of
existing documents. Not recommended to use. Please use very carefully to ensure document integrity.
:param mongo_conn: The instance of the database connector to use for the operation.
:param mongo_operations: The list operations that are supported by MongoDB's Bulk Write system.
:return: The no. of documents affected.
"""
return await mongo_conn.bulk_write( return await mongo_conn.bulk_write(
collection = self.MESSAGES_COLLECTION, collection = self.MESSAGES_COLLECTION,
requests = mongo_operations, requests = mongo_operations,
@@ -195,7 +210,7 @@ class MessageController(BaseModel):
) -> List[CoreMessageModel] | None: ) -> List[CoreMessageModel] | None:
""" """
Fetches many messages in one call, but leaves out the full payloads. This does not mark messages as read. Fetches many messages in one call, but leaves out the full payloads.
:param mongo_conn: The instance of the database connector to use for the operation. :param mongo_conn: The instance of the database connector to use for the operation.
:param token_ids: The token ids of the accounts from which these messages must be fetched. :param token_ids: The token ids of the accounts from which these messages must be fetched.
:param limit: The max. no. of messages to retrieve in this call. :param limit: The max. no. of messages to retrieve in this call.
@@ -223,8 +238,8 @@ class MessageController(BaseModel):
projection = { projection = {
"_id": True, "_id": True,
"ts": True, "ts": True,
"syncTs": True,
"tokenId": True, "tokenId": True,
"markedAsUnread": True,
"serviceType": True, "serviceType": True,
"client": True, "client": True,
"clientMessageId": True, "clientMessageId": True,
@@ -232,10 +247,11 @@ class MessageController(BaseModel):
"isSent": True, "isSent": True,
"isBroadcast": True, "isBroadcast": True,
"sentSuccessfully": True, "sentSuccessfully": True,
"sender": True,
"chat": True,
"snippet": True,
"aiSnippet": True, "aiSnippet": True,
"preview": True, "tags": True
"tags": True,
"usedAi": True
}, },
raise_exception = True raise_exception = True
) )
@@ -263,9 +279,6 @@ class MessageController(BaseModel):
:return: The list of messages (as the message model). This list can be empty. :return: The list of messages (as the message model). This list can be empty.
""" """
# Note down the timestamp at which this event occurred:
request_ts = date_time.get_current_utc_date_time(as_string = False)
# Prepare the filter: # Prepare the filter:
if not isinstance(token_ids, list): token_ids = [token_ids] if not isinstance(token_ids, list): token_ids = [token_ids]
token_ids = [ObjectId(t) for t in token_ids] token_ids = [ObjectId(t) for t in token_ids]
@@ -285,75 +298,31 @@ class MessageController(BaseModel):
raise_exception = True raise_exception = True
) )
# We now mark these fetched messages as read through a bulk-write operation:
operations = [
UpdateOne(
filter = {"_id": record["_id"]},
update = [{
"$set": {
"readTs": {
"$cond": {
"if": {
"$or": [
{"$eq": ["$readTs", None]},
{"$eq": [{"$type": "$readTs"}, "missing"]}
]
},
"then": request_ts,
"else": "$readTs"
}
}
}
}],
upsert = False
) for record in records
]
updated_count = await mongo_conn.bulk_write(
collection = self.MESSAGES_COLLECTION,
requests = operations,
raise_exception = True
)
# Convert the fetched records to instances of the data model and return: # Convert the fetched records to instances of the data model and return:
return [CoreMessageModel(**record) for record in records] return [CoreMessageModel(**record) for record in records]
async def get_message( async def get_message(
self, self,
mongo_conn: AsyncMongo, mongo_conn: AsyncMongo,
token_id: ObjectId | str,
message_id: ObjectId | str, message_id: ObjectId | str,
) -> CoreMessageModel | None: ) -> CoreMessageModel | None:
""" """
Gets one message if you know its message id. Marks that message as read. Gets one message if you know its message id.
:param mongo_conn: :param mongo_conn: The instance of the database connector to use for the operation.
:param message_id: :param token_id: The id of the auth-token associated with the message. Needed for security.
:return: :param message_id: The id of the message that needs to be read.
:return: The contents of that one message in a structured format.
""" """
# Note down the timestamp at which this event occurred: # We fetch the whole payload of that one message:
request_ts = date_time.get_current_utc_date_time(as_string = False) record = await mongo_conn.find_one(
# We fetch the whole payload of that one message
# while also marking it as read if not already marked:
record = await mongo_conn.find_one_and_update(
collection = self.MESSAGES_COLLECTION, collection = self.MESSAGES_COLLECTION,
filter = {"_id": ObjectId(message_id)}, filter = {
update = [{ "_id": ObjectId(message_id),
"$set": { "tokenId": ObjectId(token_id)
"readTs": {
"$cond": {
"if": {
"$or": [
{"$eq": ["$readTs", None]},
{"$eq": [{"$type": "$readTs"}, "missing"]}
]
}, },
"then": request_ts,
"else": "$readTs"
}
}
}
}],
raise_exception = True raise_exception = True
) )
@@ -372,6 +341,57 @@ class MessageController(BaseModel):
# We don't support updating messages themselves, # We don't support updating messages themselves,
# but we will allow updating fields like tags, marking as read or unread, etc. # but we will allow updating fields like tags, marking as read or unread, etc.
async def update_tags(
self,
mongo_conn: AsyncMongo,
token_id: ObjectId | str,
message_id: ObjectId | str,
unset_tags: List[str] = None,
set_tags: List[str] = None
) -> bool:
"""
Updates the tags on one message. The tags to remove are processed first, the ones to add are processed later.
:param mongo_conn: The instance of the database connector to use for the operation.
:param token_id: The id of the auth-token associated with the message. Needed for security.
:param message_id: The id of the message that needs to be read.
:param unset_tags: The tags to remove from the message.
:param set_tags: The tags to add to the message.
:return: True if the update was successful, else False.
"""
# Update the tags:
return await mongo_conn.update_one(
collection = self.MESSAGES_COLLECTION,
filter = {
"_id": ObjectId(message_id),
"tokenId": ObjectId(token_id)
},
update = [{
"$set": {
"tags": {
"$let": {
"vars": {
"removed_tags": {
"$setDifference": [
"$tags",
unset_tags
]
}
},
"in": {
"$setUnion": [
"$$removed_tags",
set_tags
]
}
}
}
}
}],
raise_exception = True
)
# ┏┓┳┓┳┳┳┓ ┳┓ ┓ # ┏┓┳┓┳┳┳┓ ┳┓ ┓
# ┃ ┣┫┃┃┃┃ ━━ ┃┃┏┓┃┏┓╋┏┓ # ┃ ┣┫┃┃┃┃ ━━ ┃┃┏┓┃┏┓╋┏┓
# ┗┛┛┗┗┛┻┛ ┻┛┗ ┗┗ ┗┗ # ┗┛┛┗┗┛┻┛ ┻┛┗ ┗┗ ┗┗
+10 -4
View File
@@ -37,7 +37,7 @@ sys.path.append("..")
# For making data behaviour_models: # For making data behaviour_models:
from pydantic import BaseModel, Field, field_validator, PastDatetime from pydantic import BaseModel, Field, field_validator, PastDatetime
from typing import Optional, Literal, List from typing import Optional, Literal, List, Any
# My utils: # My utils:
from utils_v2.string import regex from utils_v2.string import regex
@@ -99,10 +99,10 @@ class MailListRequestHeaders(BaseModel):
# --------------------------------------------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------------------------------------------
class MailListByAccountIdRequestData(BaseModel): class MailListRequestData(BaseModel):
tokenId: str | List[str] = Field( tokenIds: str | List[str] = Field(
description = "the account identifier(s) (Mongo ObjectId) granted by 'MailOAuthModel.get_account_identifier'", description = "the token identifier(s) that tell you which auth-tokens were used for fetching those messages",
frozen = True frozen = True
) )
@@ -116,10 +116,16 @@ class MailListByAccountIdRequestData(BaseModel):
fromCount: int = Field( fromCount: int = Field(
description = "the no. of mails to skip before picking mails to list; useful for pagination", description = "the no. of mails to skip before picking mails to list; useful for pagination",
ge = 0,
default = 0, default = 0,
frozen = True frozen = True
) )
tags: List[Any] | None = Field(
description = "any no. of tags that you want to filter by",
default = None
)
# ┏┓ ┏• # ┏┓ ┏•
# ┃ ┏┓┏┓╋┓┏┓ # ┃ ┏┓┏┓╋┓┏┓
# ┗┛┗┛┛┗┛┗┗┫ # ┗┛┗┛┛┗┛┗┗┫
+17 -5
View File
@@ -6,11 +6,11 @@
DATE: DATE:
Tuesday, 3rd Dec., 2024. Friday, 13th Dec., 2024.
OBJECTIVE: OBJECTIVE:
To provide a structure to query the full payload of an email. To provide a structure to work with the tags on mail messages.
REFERENCES: REFERENCES:
@@ -37,7 +37,7 @@ sys.path.append("..")
# For making data behaviour_models: # For making data behaviour_models:
from pydantic import BaseModel, Field, field_validator, PastDatetime from pydantic import BaseModel, Field, field_validator, PastDatetime
from typing import Optional, Literal from typing import Optional, Literal, List, Any
# My utils: # My utils:
from utils_v2.string import regex from utils_v2.string import regex
@@ -75,7 +75,7 @@ REGEX_SESSION_TOKEN = r"^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]
# ***************************************************************************************************************** # *****************************************************************************************************************
class MailGetRequestHeaders(BaseModel): class MailUpdateTagsRequestHeaders(BaseModel):
sessionToken: str = Field( sessionToken: str = Field(
description = "the session token of the user who is requesting the service", description = "the session token of the user who is requesting the service",
@@ -99,7 +99,7 @@ class MailGetRequestHeaders(BaseModel):
# --------------------------------------------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------------------------------------------
class MailGetRequestData(BaseModel): class MailUpdateTagsRequestData(BaseModel):
tokenId: str = Field( tokenId: str = Field(
description = "the id of the token associated with the mail; needed for security", description = "the id of the token associated with the mail; needed for security",
@@ -111,6 +111,18 @@ class MailGetRequestData(BaseModel):
frozen = True frozen = True
) )
unsetTags: List[Any] | None = Field(
description = "the list of tags to remove from the mail",
frozen = True,
default = None
)
setTags: List[Any] | None = Field(
description = "the list of tags to add to the mail",
frozen = True,
default = None
)
# ┏┓ ┏• # ┏┓ ┏•
# ┃ ┏┓┏┓╋┓┏┓ # ┃ ┏┓┏┓╋┓┏┓
# ┗┛┗┛┛┗┛┗┗┫ # ┗┛┗┛┛┗┛┗┗┫
+70 -30
View File
@@ -36,7 +36,7 @@ sys.path.append(".")
sys.path.append("..") sys.path.append("..")
# For making data behaviour_models: # For making data behaviour_models:
from pydantic import BaseModel, Field, field_validator, PastDatetime, AwareDatetime from pydantic import BaseModel, Field, field_validator, PastDatetime, AwareDatetime, constr
from typing import Optional, Literal, Union, List, Any from typing import Optional, Literal, Union, List, Any
# My utils: # My utils:
@@ -100,18 +100,6 @@ class CoreMessageModel(BaseModel):
default_factory = lambda: date_time.get_current_utc_date_time(as_string = False) default_factory = lambda: date_time.get_current_utc_date_time(as_string = False)
) )
readTs: AwareDatetime | None = Field(
description = "the time (utc) at which this message was read by the user",
frozen = True,
default = None
)
markedAsUnread: bool = Field(
description = "to note when the user has marked this message as unread",
frozen = False,
default = False
)
tokenId: ObjectId = Field( tokenId: ObjectId = Field(
description = "the id of the auth token that is associated with this message", description = "the id of the auth token that is associated with this message",
frozen = True frozen = True
@@ -160,20 +148,30 @@ class CoreMessageModel(BaseModel):
default = False default = False
) )
aiSnippet: LLMOutput | dict | None = Field( sender: str | None = Field(
description = "holds a short summary generated by ", description = "the name of the sender; null if you are the sender",
frozen = False, frozen = True
default = None
) )
preview: str = Field( chat: str | None = Field(
description = "the name of the chat where the message was exchanged; relevant in chat apps like telegram",
frozen = True
)
message: dict = Field(
description = "the actual content(s) of the message; will differ for each service/client",
frozen = True
)
snippet: str = Field(
description = "a truncated version of the actual textual content of the message", description = "a truncated version of the actual textual content of the message",
frozen = False frozen = False
) )
message: dict = Field( aiSnippet: LLMOutput | dict | None = Field(
description = "the actual contents of the message; will differ for each client", description = "holds a short summary generated by an llm",
frozen = True frozen = False,
default = None
) )
tags: List[Any] = Field( tags: List[Any] = Field(
@@ -183,29 +181,67 @@ class CoreMessageModel(BaseModel):
examples = ["urgent", "otp", "GST"] examples = ["urgent", "otp", "GST"]
) )
usedAi: bool | None = Field(
description = "to mark when a sent message was generated by ai; null means the status is not known",
frozen = True,
default = None
)
# ┏┓ ┏• # ┏┓ ┏•
# ┃ ┏┓┏┓╋┓┏┓ # ┃ ┏┓┏┓╋┓┏┓
# ┗┛┗┛┛┗┛┗┗┫ # ┗┛┗┛┛┗┛┗┗┫
# ┛ # ┛
class Config: class Config:
extra = "allow" extra = "forbid"
arbitrary_types_allowed = True arbitrary_types_allowed = True
def model_dump(self, *args, **kwargs): def model_dump(self, *args, **kwargs):
return super().model_dump(*args, by_alias = True, **kwargs) return super().model_dump(*args, by_alias = True, **kwargs)
# ┏┓ •
# ┃┃┏┓┏┓┏┓┏┓┏┓╋┓┏┓┏
# ┣┛┛ ┗┛┣┛┗ ┛ ┗┗┗ ┛
# ┛
@property
def full(self):
return {
"messageId": str(self.messageId),
"ts": self.ts.isoformat(),
"serviceType": self.serviceType,
"client": self.client,
"clientMessageId": self.clientMessageId,
"clientThreadId": self.clientThreadId,
"isSent": self.isSent,
"isBroadcast": self.isBroadcast,
"sentSuccessfully": self.sentSuccessfully,
"sender": self.sender,
"chat": self.chat,
"message": self.message,
"snippet": self.snippet,
"aiSnippet": self.aiSnippet,
"tags": self.tags
}
@property
def preview(self):
return {
"messageId": str(self.messageId),
"ts": self.ts.isoformat(),
"serviceType": self.serviceType,
"client": self.client,
"clientMessageId": self.clientMessageId,
"clientThreadId": self.clientThreadId,
"isSent": self.isSent,
"isBroadcast": self.isBroadcast,
"sentSuccessfully": self.sentSuccessfully,
"sender": self.sender,
"chat": self.chat,
"snippet": self.snippet,
"aiSnippet": self.aiSnippet,
"tags": self.tags
}
# ┓┏ ┓• ┓ • # ┓┏ ┓• ┓ •
# ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓
# ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗
@field_validator("ts", "syncTs", "readTs", mode = "before") @field_validator("ts", "syncTs", mode = "before")
def parse_date_time(cls, value): def parse_date_time(cls, value):
return date_time.parse_date_time(input_value = value, timezone = date_time.TIMEZONE_UTC) return date_time.parse_date_time(input_value = value, timezone = date_time.TIMEZONE_UTC)
@@ -231,6 +267,7 @@ class CoreMessageModel(BaseModel):
if __name__ == "__main__": if __name__ == "__main__":
from utils_v2.string import json from utils_v2.string import json
message = CoreMessageModel( message = CoreMessageModel(
@@ -245,7 +282,10 @@ if __name__ == "__main__":
"from": "bhopli@gmil.com", "from": "bhopli@gmil.com",
"to": "hello@thecaoffice.com", "to": "hello@thecaoffice.com",
"message": "Hello, World!" "message": "Hello, World!"
} },
sender = "Polki",
chat = "T6 Cats",
preview = "Hi, there! How do you do?"
) )
print("MESSAGE MODEL:", json.to_string(message.model_dump(), default = str)) print("MESSAGE MODEL:", json.to_string(message.model_dump(), default = str))