# ============================================================ # app/models/message.py - Message Model # ============================================================ from datetime import datetime from typing import Optional, List, Dict # Time windows for message actions (in minutes) EDIT_WINDOW_MINUTES = 15 # Can edit within 15 minutes DELETE_FOR_EVERYONE_WINDOW_MINUTES = 60 # Can delete for everyone within 1 hour class Message: """Message model for MongoDB""" @staticmethod def create_document( conversation_id: str, sender_id: str, sender_name: str, sender_avatar: Optional[str], message_type: str, content: Optional[str] = None, media: Optional[dict] = None, property_card: Optional[dict] = None, replied_to_message_id: Optional[str] = None, replied_to_content: Optional[str] = None, replied_to_sender: Optional[str] = None, ) -> dict: """Create message document for insertion""" now = datetime.utcnow() return { "conversation_id": conversation_id, "sender_id": sender_id, "sender_name": sender_name, "sender_avatar": sender_avatar, "message_type": message_type, # "text", "image", "document", "voice", "property_inquiry", "system" "content": content, "media": media, # {url, filename, size, mime_type, duration, thumbnail_url} "property_card": property_card, # {listing_id, title, price, currency, bedrooms, bathrooms, location, image_url, listing_type} # Reply-to fields "replied_to_message_id": replied_to_message_id, "replied_to_content": replied_to_content, # Preview of replied message (truncated) "replied_to_sender": replied_to_sender, # Name of original sender "is_read": False, "read_at": None, "read_by": [], # List of user IDs who have read this message "delivered_to": [], # List of user IDs who have received this message (online) # Edit tracking "is_edited": False, "edited_at": None, # Deletion tracking "is_deleted": False, # Deleted for everyone "deleted_at": None, "deleted_for": [], # List of user_ids who deleted this message for themselves # Reactions: {emoji: [user_id1, user_id2, ...]} "reactions": {}, "created_at": now, } @staticmethod def _serialize_datetime(dt) -> Optional[str]: """Convert datetime to ISO string, handling None values""" if dt is None: return None if isinstance(dt, datetime): return dt.isoformat() return dt # Already a string or other serializable type @staticmethod def _calculate_action_availability( message_doc: dict, for_user_id: Optional[str] = None ) -> dict: """ Calculate whether edit and delete-for-everyone actions are available. Returns: dict with can_edit and can_delete_for_everyone flags """ is_deleted = message_doc.get("is_deleted", False) sender_id = message_doc.get("sender_id") message_type = message_doc.get("message_type") created_at = message_doc.get("created_at") # Default: no actions available can_edit = False can_delete_for_everyone = False # If message is deleted, no actions available if is_deleted: return {"can_edit": False, "can_delete_for_everyone": False} # Only sender can edit/delete for everyone is_sender = for_user_id and sender_id == for_user_id if is_sender and created_at: now = datetime.utcnow() minutes_since = (now - created_at).total_seconds() / 60 # Edit: only text messages, within 15 minutes if message_type == "text" and minutes_since <= EDIT_WINDOW_MINUTES: can_edit = True # Delete for everyone: within 1 hour if minutes_since <= DELETE_FOR_EVERYONE_WINDOW_MINUTES: can_delete_for_everyone = True return { "can_edit": can_edit, "can_delete_for_everyone": can_delete_for_everyone, } @staticmethod def format_response(message_doc: dict, for_user_id: Optional[str] = None) -> Optional[dict]: """ Format message document for API response. Args: message_doc: The message document from MongoDB for_user_id: If provided, checks if message is deleted for this user and calculates action availability Returns: Formatted message dict, or None if deleted for the requesting user """ if not message_doc: return None # If message is deleted for everyone, show placeholder is_deleted = message_doc.get("is_deleted", False) # If deleted specifically for this user, don't return the message deleted_for = message_doc.get("deleted_for", []) if for_user_id and for_user_id in deleted_for: return None # Message was "deleted for me" by this user # Calculate action availability for frontend action_availability = Message._calculate_action_availability(message_doc, for_user_id) # Build response response = { "id": str(message_doc.get("_id", "")), "conversation_id": message_doc.get("conversation_id"), "sender_id": message_doc.get("sender_id"), "sender_name": message_doc.get("sender_name"), "sender_avatar": message_doc.get("sender_avatar"), "message_type": message_doc.get("message_type"), "content": message_doc.get("content") if not is_deleted else None, "media": message_doc.get("media") if not is_deleted else None, "property_card": message_doc.get("property_card") if not is_deleted else None, # CRITICAL: Include metadata for alert_results, search_results, etc. "metadata": message_doc.get("metadata") if not is_deleted else None, # Reply-to fields "replied_to_message_id": message_doc.get("replied_to_message_id"), "replied_to_content": message_doc.get("replied_to_content"), "replied_to_sender": message_doc.get("replied_to_sender"), "is_read": message_doc.get("is_read", False), "read_at": Message._serialize_datetime(message_doc.get("read_at")), # Edit fields "is_edited": message_doc.get("is_edited", False), "edited_at": Message._serialize_datetime(message_doc.get("edited_at")), # Deletion status "is_deleted": is_deleted, "deleted_at": Message._serialize_datetime(message_doc.get("deleted_at")) if is_deleted else None, # Reactions "reactions": message_doc.get("reactions", {}), "created_at": Message._serialize_datetime(message_doc.get("created_at")), # Status field for read receipts - calculated from sender's perspective "status": Message._calculate_status(message_doc, for_user_id), # Action availability flags for frontend UI "can_edit": action_availability["can_edit"], "can_delete_for_everyone": action_availability["can_delete_for_everyone"], } return response @staticmethod def _calculate_status(message_doc: dict, for_user_id: Optional[str] = None) -> str: """ Calculate message status from user's perspective. Status indicators: - sent: Message saved to database (single gray check) - delivered: Message received by recipient (double gray check) - read: Recipient has read the message (double blue check) For sender: Show read if recipient has read, otherwise delivered/sent For recipient: Always delivered (they have it) """ sender_id = message_doc.get("sender_id") read_by = message_doc.get("read_by", []) delivered_to = message_doc.get("delivered_to", []) # If no for_user_id provided, default to "sent" (can't determine perspective) if not for_user_id: return "sent" # If viewing as sender if for_user_id == sender_id: # Check if anyone else has read it other_readers = [uid for uid in read_by if uid != sender_id] if other_readers: return "read" # Recipient has read it - double blue check # Check if anyone else has received it (delivered) other_recipients = [uid for uid in delivered_to if uid != sender_id] if other_recipients: return "delivered" # Recipient received it - double gray check return "sent" # Not delivered yet - single gray check else: # Viewing as recipient - they have the message return "delivered"