(20241213) Mail Module Reworked!
This commit is contained in:
@@ -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")
|
||||
@read_input(sanitize_headers = False, sanitize_data = False)
|
||||
@get_session_info(key = "X-Session-Token", session_coro = "get_session")
|
||||
|
||||
@@ -81,7 +81,7 @@ import asyncio
|
||||
|
||||
|
||||
# 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):
|
||||
|
||||
# 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")
|
||||
@read_input(sanitize_headers = False, sanitize_data = False)
|
||||
@log_request_to_mongo(
|
||||
|
||||
@@ -85,7 +85,7 @@ import asyncio
|
||||
|
||||
|
||||
# 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):
|
||||
|
||||
# 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")
|
||||
@read_input(sanitize_headers = False, sanitize_data = False)
|
||||
@get_session_info(key = "X-Session-Token", session_coro = "get_session")
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
|
||||
# To make sibling directories accessible for imports:
|
||||
import sys
|
||||
sys.path.append(".")
|
||||
sys.path.append("..")
|
||||
sys.path.append("../..")
|
||||
|
||||
# For using Quart:
|
||||
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")
|
||||
@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)
|
||||
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 | MailGetRequestHeaders = None,
|
||||
inbound_data: dict | MailGetRequestData = None,
|
||||
inbound_headers: dict | MailUpdateTagsRequestHeaders = None,
|
||||
inbound_data: dict | MailUpdateTagsRequestData = None,
|
||||
inbound_files: dict = None,
|
||||
**kwargs
|
||||
):
|
||||
@@ -164,17 +164,18 @@ async def update_mail_tags(
|
||||
)
|
||||
|
||||
# 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,
|
||||
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:
|
||||
return ResponseModel(
|
||||
status_code = StatusCodes.OK if message else StatusCodes.FAILED,
|
||||
http_code = HttpCodes.SUCCESS if message else HttpCodes.NOT_FOUND,
|
||||
data = message.full
|
||||
status_code = StatusCodes.OK if success else StatusCodes.FAILED,
|
||||
http_code = HttpCodes.SUCCESS if success else HttpCodes.INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
@read_input(sanitize_headers = False, sanitize_data = False)
|
||||
@get_session_info(key = "X-Session-Token", session_coro = "get_session")
|
||||
@@ -131,21 +131,7 @@ async def callback_test(
|
||||
**kwargs
|
||||
):
|
||||
|
||||
print("SESSION INFO:", kwargs.get("session_info"))
|
||||
|
||||
# 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"
|
||||
])
|
||||
)
|
||||
return f"ok ({request.method})"
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
|
||||
+16
-22
@@ -42,29 +42,21 @@ import os
|
||||
import psutil
|
||||
|
||||
# For using Quart:
|
||||
from quart import Quart, request, current_app
|
||||
from quart import Quart, current_app
|
||||
from quart_cors import cors
|
||||
|
||||
# Common:
|
||||
from shared import constants
|
||||
|
||||
# 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.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.cache.async_redis_cache_v2 import AsyncRedisCache
|
||||
from utils_v2.serialization.json_serializer import JSONSerializer
|
||||
from utils_v2.api.async_quart import (
|
||||
set_api_version,
|
||||
read_input,
|
||||
log_request_to_mongo,
|
||||
should_not_be_under_maintenance,
|
||||
only_whitelisted_ips,
|
||||
limit_rate,
|
||||
validate_input,
|
||||
handle_cancelled_request
|
||||
log_request_to_mongo
|
||||
)
|
||||
|
||||
# GMail-related utils:
|
||||
@@ -92,11 +84,12 @@ import httpx
|
||||
from icecream import IceCreamDebugger
|
||||
|
||||
# All the blueprints:
|
||||
from api.blueprints.mail.oauth_request import mail_oauth_bp
|
||||
from api.blueprints.mail.oauth_callback import mail_callback_bp
|
||||
from api.blueprints.mail.sync_v2 import mail_sync_bp
|
||||
# from api.blueprints.mail.list import mail_list_bp
|
||||
# from api.blueprints.mail.retrieve import mail_retrieve_bp
|
||||
from api.blueprints.mail.oauth.request import mail_oauth_request_bp
|
||||
from api.blueprints.mail.oauth.callback import mail_oauth_callback_bp
|
||||
from api.blueprints.mail.sync.sync_v2 import mail_sync_bp
|
||||
from api.blueprints.mail.retrieve.list import mail_list_bp
|
||||
from api.blueprints.mail.retrieve.get import mail_get_bp
|
||||
from api.blueprints.mail.tags.update import mail_tags_update_bp
|
||||
# from api.blueprints.sms.auth import sms_auth_bp
|
||||
# from api.blueprints.sms.send import sms_send_bp
|
||||
# from api.blueprints.chat.auth import chat_auth_bp
|
||||
@@ -131,11 +124,12 @@ APP_VERSION = constants.APP_VERSION
|
||||
# The Quart app:
|
||||
app = Quart(__name__, template_folder = r"../views")
|
||||
app = cors(app)
|
||||
app.register_blueprint(mail_oauth_bp, url_prefix = f"/{MODULE_BASE}/mail")
|
||||
app.register_blueprint(mail_callback_bp, url_prefix = f"/{MODULE_BASE}/mail")
|
||||
app.register_blueprint(mail_oauth_request_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_list_bp, url_prefix = f"/{MODULE_BASE}/mail")
|
||||
# app.register_blueprint(mail_retrieve_bp, url_prefix = f"/{MODULE_BASE}/mail")
|
||||
app.register_blueprint(mail_list_bp, url_prefix = f"/{MODULE_BASE}/mail")
|
||||
app.register_blueprint(mail_get_bp, url_prefix = f"/{MODULE_BASE}/mail")
|
||||
app.register_blueprint(mail_tags_update_bp, url_prefix = f"/{MODULE_BASE}/mail")
|
||||
# app.register_blueprint(sms_auth_bp, url_prefix = f"/{MODULE_BASE}/sms")
|
||||
# app.register_blueprint(sms_send_bp, url_prefix = f"/{MODULE_BASE}/sms")
|
||||
# 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,
|
||||
alert_url = current_app.script_data["alerts"]["url"],
|
||||
http_client = current_app.http_client,
|
||||
@@ -337,7 +331,7 @@ async def app_startup(**kwargs):
|
||||
debug_prefix = "Message (CM) | ",
|
||||
debug_only_errors = True
|
||||
)
|
||||
current_app.core_auth_token_controller = AuthTokenController(
|
||||
current_app.core_message_controller = MessageController(
|
||||
cache = current_app.module_cache,
|
||||
alert_url = current_app.script_data["alerts"]["url"],
|
||||
http_client = current_app.http_client,
|
||||
|
||||
+78
-5
@@ -283,14 +283,12 @@ class MailController:
|
||||
async def get_token(
|
||||
mongo_conn: AsyncMongo,
|
||||
token_id: ObjectId | str = None,
|
||||
**kwargs
|
||||
) -> CoreAuthTokenModel | None:
|
||||
|
||||
# Simply call the core model:
|
||||
return await current_app.core_auth_token_controller.get_token(
|
||||
mongo_conn = mongo_conn,
|
||||
token_id = token_id,
|
||||
**kwargs
|
||||
token_id = token_id
|
||||
)
|
||||
|
||||
# ┏┓ ┳┳┓
|
||||
@@ -364,8 +362,15 @@ class MailController:
|
||||
client = auth_token.client,
|
||||
clientMessageId = message_id,
|
||||
clientThreadId = client_response.data["threadId"],
|
||||
preview = client_response.data["subject"],
|
||||
message = client_response.data
|
||||
isSent = False,
|
||||
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:
|
||||
@@ -564,6 +569,74 @@ class MailController:
|
||||
sync_results.message = f"no such mail client '{auth_token.client}'"
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
|
||||
@@ -272,30 +272,20 @@ class AuthTokenController(BaseModel):
|
||||
self,
|
||||
mongo_conn: AsyncMongo,
|
||||
token_id: ObjectId | str = None,
|
||||
**kwargs
|
||||
) -> CoreAuthTokenModel | None:
|
||||
|
||||
"""
|
||||
To retrieve stored tokens from the database.
|
||||
: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 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
|
||||
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:
|
||||
token = await mongo_conn.find_one(
|
||||
collection = self.AUTH_COLLECTION,
|
||||
filter = filter_json,
|
||||
filter = {"_id": ObjectId(token_id)},
|
||||
)
|
||||
|
||||
# Done here:
|
||||
|
||||
+84
-64
@@ -129,6 +129,13 @@ class MessageController(BaseModel):
|
||||
message: CoreMessageModel
|
||||
) -> 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:
|
||||
return await mongo_conn.insert_one(
|
||||
collection = self.MESSAGES_COLLECTION,
|
||||
@@ -139,9 +146,17 @@ class MessageController(BaseModel):
|
||||
async def bulk_write(
|
||||
self,
|
||||
mongo_conn: AsyncMongo,
|
||||
mongo_operations
|
||||
mongo_operations: list
|
||||
) -> 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(
|
||||
collection = self.MESSAGES_COLLECTION,
|
||||
requests = mongo_operations,
|
||||
@@ -195,7 +210,7 @@ class MessageController(BaseModel):
|
||||
) -> 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 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.
|
||||
@@ -223,8 +238,8 @@ class MessageController(BaseModel):
|
||||
projection = {
|
||||
"_id": True,
|
||||
"ts": True,
|
||||
"syncTs": True,
|
||||
"tokenId": True,
|
||||
"markedAsUnread": True,
|
||||
"serviceType": True,
|
||||
"client": True,
|
||||
"clientMessageId": True,
|
||||
@@ -232,10 +247,11 @@ class MessageController(BaseModel):
|
||||
"isSent": True,
|
||||
"isBroadcast": True,
|
||||
"sentSuccessfully": True,
|
||||
"sender": True,
|
||||
"chat": True,
|
||||
"snippet": True,
|
||||
"aiSnippet": True,
|
||||
"preview": True,
|
||||
"tags": True,
|
||||
"usedAi": True
|
||||
"tags": 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.
|
||||
"""
|
||||
|
||||
# Note down the timestamp at which this event occurred:
|
||||
request_ts = date_time.get_current_utc_date_time(as_string = False)
|
||||
|
||||
# Prepare the filter:
|
||||
if not isinstance(token_ids, list): token_ids = [token_ids]
|
||||
token_ids = [ObjectId(t) for t in token_ids]
|
||||
@@ -285,75 +298,31 @@ class MessageController(BaseModel):
|
||||
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:
|
||||
return [CoreMessageModel(**record) for record in records]
|
||||
|
||||
async def get_message(
|
||||
self,
|
||||
mongo_conn: AsyncMongo,
|
||||
token_id: ObjectId | str,
|
||||
message_id: ObjectId | str,
|
||||
) -> CoreMessageModel | None:
|
||||
|
||||
"""
|
||||
Gets one message if you know its message id. Marks that message as read.
|
||||
:param mongo_conn:
|
||||
:param message_id:
|
||||
:return:
|
||||
Gets one message if you know its message id.
|
||||
: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.
|
||||
:return: The contents of that one message in a structured format.
|
||||
"""
|
||||
|
||||
# Note down the timestamp at which this event occurred:
|
||||
request_ts = date_time.get_current_utc_date_time(as_string = False)
|
||||
|
||||
# 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(
|
||||
# We fetch the whole payload of that one message:
|
||||
record = await mongo_conn.find_one(
|
||||
collection = self.MESSAGES_COLLECTION,
|
||||
filter = {"_id": ObjectId(message_id)},
|
||||
update = [{
|
||||
"$set": {
|
||||
"readTs": {
|
||||
"$cond": {
|
||||
"if": {
|
||||
"$or": [
|
||||
{"$eq": ["$readTs", None]},
|
||||
{"$eq": [{"$type": "$readTs"}, "missing"]}
|
||||
]
|
||||
filter = {
|
||||
"_id": ObjectId(message_id),
|
||||
"tokenId": ObjectId(token_id)
|
||||
},
|
||||
"then": request_ts,
|
||||
"else": "$readTs"
|
||||
}
|
||||
}
|
||||
}
|
||||
}],
|
||||
raise_exception = True
|
||||
)
|
||||
|
||||
@@ -372,6 +341,57 @@ class MessageController(BaseModel):
|
||||
# We don't support updating messages themselves,
|
||||
# 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
@@ -37,7 +37,7 @@ sys.path.append("..")
|
||||
|
||||
# For making data behaviour_models:
|
||||
from pydantic import BaseModel, Field, field_validator, PastDatetime
|
||||
from typing import Optional, Literal, List
|
||||
from typing import Optional, Literal, List, Any
|
||||
|
||||
# My utils:
|
||||
from utils_v2.string import regex
|
||||
@@ -99,10 +99,10 @@ class MailListRequestHeaders(BaseModel):
|
||||
# ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
class MailListByAccountIdRequestData(BaseModel):
|
||||
class MailListRequestData(BaseModel):
|
||||
|
||||
tokenId: str | List[str] = Field(
|
||||
description = "the account identifier(s) (Mongo ObjectId) granted by 'MailOAuthModel.get_account_identifier'",
|
||||
tokenIds: str | List[str] = Field(
|
||||
description = "the token identifier(s) that tell you which auth-tokens were used for fetching those messages",
|
||||
frozen = True
|
||||
)
|
||||
|
||||
@@ -116,10 +116,16 @@ class MailListByAccountIdRequestData(BaseModel):
|
||||
|
||||
fromCount: int = Field(
|
||||
description = "the no. of mails to skip before picking mails to list; useful for pagination",
|
||||
ge = 0,
|
||||
default = 0,
|
||||
frozen = True
|
||||
)
|
||||
|
||||
tags: List[Any] | None = Field(
|
||||
description = "any no. of tags that you want to filter by",
|
||||
default = None
|
||||
)
|
||||
|
||||
# ┏┓ ┏•
|
||||
# ┃ ┏┓┏┓╋┓┏┓
|
||||
# ┗┛┗┛┛┗┛┗┗┫
|
||||
|
||||
+17
-5
@@ -6,11 +6,11 @@
|
||||
|
||||
DATE:
|
||||
|
||||
Tuesday, 3rd Dec., 2024.
|
||||
Friday, 13th Dec., 2024.
|
||||
|
||||
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:
|
||||
|
||||
@@ -37,7 +37,7 @@ sys.path.append("..")
|
||||
|
||||
# For making data behaviour_models:
|
||||
from pydantic import BaseModel, Field, field_validator, PastDatetime
|
||||
from typing import Optional, Literal
|
||||
from typing import Optional, Literal, List, Any
|
||||
|
||||
# My utils:
|
||||
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(
|
||||
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(
|
||||
description = "the id of the token associated with the mail; needed for security",
|
||||
@@ -111,6 +111,18 @@ class MailGetRequestData(BaseModel):
|
||||
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
@@ -36,7 +36,7 @@ sys.path.append(".")
|
||||
sys.path.append("..")
|
||||
|
||||
# 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
|
||||
|
||||
# My utils:
|
||||
@@ -100,18 +100,6 @@ class CoreMessageModel(BaseModel):
|
||||
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(
|
||||
description = "the id of the auth token that is associated with this message",
|
||||
frozen = True
|
||||
@@ -160,20 +148,30 @@ class CoreMessageModel(BaseModel):
|
||||
default = False
|
||||
)
|
||||
|
||||
aiSnippet: LLMOutput | dict | None = Field(
|
||||
description = "holds a short summary generated by ",
|
||||
frozen = False,
|
||||
default = None
|
||||
sender: str | None = Field(
|
||||
description = "the name of the sender; null if you are the sender",
|
||||
frozen = True
|
||||
)
|
||||
|
||||
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",
|
||||
frozen = False
|
||||
)
|
||||
|
||||
message: dict = Field(
|
||||
description = "the actual contents of the message; will differ for each client",
|
||||
frozen = True
|
||||
aiSnippet: LLMOutput | dict | None = Field(
|
||||
description = "holds a short summary generated by an llm",
|
||||
frozen = False,
|
||||
default = None
|
||||
)
|
||||
|
||||
tags: List[Any] = Field(
|
||||
@@ -183,29 +181,67 @@ class CoreMessageModel(BaseModel):
|
||||
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:
|
||||
extra = "allow"
|
||||
extra = "forbid"
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
def model_dump(self, *args, **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):
|
||||
return date_time.parse_date_time(input_value = value, timezone = date_time.TIMEZONE_UTC)
|
||||
|
||||
@@ -231,6 +267,7 @@ class CoreMessageModel(BaseModel):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
from utils_v2.string import json
|
||||
|
||||
message = CoreMessageModel(
|
||||
@@ -245,7 +282,10 @@ if __name__ == "__main__":
|
||||
"from": "bhopli@gmil.com",
|
||||
"to": "hello@thecaoffice.com",
|
||||
"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))
|
||||
|
||||
Reference in New Issue
Block a user