Spaces:
Running
Running
| # ============================================================ | |
| # 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""" | |
| 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, | |
| } | |
| 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 | |
| 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, | |
| } | |
| 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 | |
| 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" | |