Spaces:
Running
Running
File size: 9,361 Bytes
0a645ca 4e15311 0a645ca f7e886e 0a645ca f7e886e 0a645ca 2043cb2 310bac1 4e15311 0a645ca 4e15311 0a645ca 4e15311 0a645ca 4e15311 0a645ca 4e15311 6f7fd5d f7e886e 0a645ca 4e15311 2043cb2 4e15311 0a645ca 4e15311 2043cb2 701f9eb 2043cb2 310bac1 2043cb2 701f9eb 2043cb2 701f9eb 2043cb2 701f9eb 310bac1 2043cb2 4e15311 701f9eb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | # ============================================================
# 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"
|