diff --git a/api/blueprints/ai/llm/invoke.py b/api/blueprints/ai/llm/invoke.py index 3f8b57d..523bed0 100644 --- a/api/blueprints/ai/llm/invoke.py +++ b/api/blueprints/ai/llm/invoke.py @@ -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") diff --git a/api/blueprints/mail/oauth/callback.py b/api/blueprints/mail/oauth/callback.py index 7958726..069e10a 100644 --- a/api/blueprints/mail/oauth/callback.py +++ b/api/blueprints/mail/oauth/callback.py @@ -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/", methods = ["POST", "GET"]) +@mail_oauth_callback_bp.route("/callback/", methods = ["POST", "GET"]) @set_api_version(api_version = "1.0.0") @read_input(sanitize_headers = False, sanitize_data = False) @log_request_to_mongo( diff --git a/api/blueprints/mail/oauth/request.py b/api/blueprints/mail/oauth/request.py index 058471c..fc8a80f 100644 --- a/api/blueprints/mail/oauth/request.py +++ b/api/blueprints/mail/oauth/request.py @@ -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") diff --git a/api/blueprints/mail/tags/update.py b/api/blueprints/mail/tags/update.py index 92abd11..e4d7100 100644 --- a/api/blueprints/mail/tags/update.py +++ b/api/blueprints/mail/tags/update.py @@ -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 ) diff --git a/api/blueprints/test/callback.py b/api/blueprints/test/callback.py index 48ee9f6..54e60c6 100644 --- a/api/blueprints/test/callback.py +++ b/api/blueprints/test/callback.py @@ -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})" # ***************************************************************************************************************** diff --git a/api/main.py b/api/main.py index b7ca673..05a1b67 100644 --- a/api/main.py +++ b/api/main.py @@ -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, diff --git a/controllers/api/mail.py b/controllers/api/mail.py index be04de5..32174fc 100644 --- a/controllers/api/mail.py +++ b/controllers/api/mail.py @@ -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 + ) + # ***************************************************************************************************************** # ***** **** diff --git a/controllers/core/auth_token.py b/controllers/core/auth_token.py index 50517c7..a7a3064 100644 --- a/controllers/core/auth_token.py +++ b/controllers/core/auth_token.py @@ -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: diff --git a/controllers/core/message.py b/controllers/core/message.py index 20bf8fa..cb21c5e 100644 --- a/controllers/core/message.py +++ b/controllers/core/message.py @@ -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"]} - ] - }, - "then": request_ts, - "else": "$readTs" - } - } - } - }], + filter = { + "_id": ObjectId(message_id), + "tokenId": ObjectId(token_id) + }, 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 + ) + # ┏┓┳┓┳┳┳┓ ┳┓ ┓ # ┃ ┣┫┃┃┃┃ ━━ ┃┃┏┓┃┏┓╋┏┓ # ┗┛┛┗┗┛┻┛ ┻┛┗ ┗┗ ┗┗ diff --git a/models/api/mail/list.py b/models/api/mail/list.py index 03081ea..3acc4a7 100644 --- a/models/api/mail/list.py +++ b/models/api/mail/list.py @@ -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 + ) + # ┏┓ ┏• # ┃ ┏┓┏┓╋┓┏┓ # ┗┛┗┛┛┗┛┗┗┫ diff --git a/models/api/mail/tags.py b/models/api/mail/tags.py index 1b4aee1..3d57f40 100644 --- a/models/api/mail/tags.py +++ b/models/api/mail/tags.py @@ -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 + ) + # ┏┓ ┏• # ┃ ┏┓┏┓╋┓┏┓ # ┗┛┗┛┛┗┛┗┗┫ diff --git a/models/core/message.py b/models/core/message.py index e1a2aaa..b64f565 100644 --- a/models/core/message.py +++ b/models/core/message.py @@ -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))