(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")
@read_input(sanitize_headers = False, sanitize_data = False)
@get_session_info(key = "X-Session-Token", session_coro = "get_session")
+3 -3
View File
@@ -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(
+3 -3
View File
@@ -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")
+11 -10
View File
@@ -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
)
+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")
@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
View File
@@ -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
View File
@@ -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
)
# *****************************************************************************************************************
# ***** ****
+1 -11
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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))