AIDA / app /models /message.py
destinyebuka's picture
fyp
310bac1
# ============================================================
# 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"